
개요
| 항목 | 내용 |
| CVE ID | CVE-2026-15748 |
| 영향 제품 | Forminator Forms – Contact Form, Payment Form & Custom Form Builder (WordPress 플러그인) |
| 영향 버전 | 1.56.1 이하 |
| 패치 버전 | 1.56.2 |
| CVSS 3.1 | 9.8 (Critical) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| 취약점 유형 | CWE-434 (위험한 유형의 파일 업로드 무제한 허용) |
| 인증 요구 여부 | 불필요 |
| 설치 규모 | 60만 개 이상의 워드프레스 사이트에 설치된 플러그인 |
| 전제조건 | 대상 폼에 File Upload 필드와 Select 필드가 함께 존재해야 함 |
Forminator Forms는 워드프레스에 문의 폼, 결제 폼, 커스텀 폼을 만들어주는 폼 빌더 플러그인입니다. 파일 첨부를 받는 폼에는 당연히 "위험한 파일은 업로드하지 못하게 막는" 로직이 들어있고, Forminator도 handle_file_upload() 함수 안에 위험 확장자 블록리스트를 두고 있습니다. 그런데 이 CVE는 단일 결함이 아니라, 서로 다른 두 개의 허술한 지점이 우연히 맞물려 RCE로 이어진 사례입니다.
하나는 업로드 필드가 "어떤 파일 타입을 허용할지"를 결정하는 설정값을, 서버에 저장된 폼 정의가 아니라 요청 자체에 담긴 값에서 신뢰한다는 점입니다. 다른 하나는 위험 확장자를 걸러내는 블록리스트 검사가 MIME 타입 문자열을 정규화 없이 키 완전 일치로만 비교한다는 점입니다. 이 둘을 조합하면, 공격자는 폼의 다른 필드(Select)에 위조된 설정값을 끼워 넣어 업로드 필드의 허용 타입을 자기 마음대로 바꾸고, 블록리스트 비교 로직의 허점을 파이프(|) 문자 하나로 피해가면서 .php 파일을 그대로 밀어 넣을 수 있습니다.
취약점이 숨어있던 자리 — 파일 업로드 필드의 "허용 타입"을 누가 결정하는가
Forminator의 폼 제출은 인증 없이 호출 가능한 admin-ajax.php 액션으로 처리됩니다. 정상적인 흐름이라면, 업로드 필드가 어떤 확장자/MIME 타입을 허용할지는 관리자가 폼을 만들 때 설정해서 서버(데이터베이스)에 저장해 둔 값이어야 하고, 제출 시점에는 그 저장된 설정을 기준으로 업로드된 파일을 검사해야 합니다.

단계별 소스코드 분석

1단계 — 공개 제출 핸들러가 "요청에 담긴" 필드 설정을 그대로 신뢰
Wordfence의 공개 권고에 따르면, 문제는 바로 이 지점에 있습니다. 공개 제출 핸들러가 업로드 필드의 설정을 서버에 저장된 값이 아니라 요청에 포함된 값으로부터 구성하는데, 이 값이 Select 필드처럼 전혀 관계없어 보이는 다른 필드를 통해 주입될 수 있다는 점입니다.

이 단계만으로도 이미 심각합니다. 관리자가 "jpg, png만 허용"으로 설정해 둔 업로드 필드라도, 공격자가 Select 필드에 조작된 값을 실어 보내면 그 폼이 실제로는 어떤 확장자를 허용하는 폼인지 자체가 요청 시점에 바뀌어버립니다.
2단계 — 그 뒤에 남은 마지막 방어선: 위험 확장자 블록리스트
허용 타입이 임의로 바뀔 수 있다 해도, 여전히 handle_file_upload()에는 .php, .phtml처럼 서버에서 실행 가능한 위험한 확장자/MIME 타입을 걸러내는 별도의 블록리스트 검사가 남아 있습니다. 바로 이 검사가 두 번째 결함의 자리입니다.

이 코드는 $mime_key가 블록리스트 배열의 키와 문자 그대로 똑같을 때만 위험하다고 판단합니다. Wordfence의 표현을 그대로 옮기면, 이 로직은 "파이프로 구분된 대체 MIME 타입 키(pipe-alternative MIME type keys)"로 우회됩니다.
정상 요청의 mime_key: "application/x-php" → $dangerous_mime_types[...] 존재 → 차단됨
조작된 요청의 mime_key: "application/x-php|" → $dangerous_mime_types[...] 없음 → isset() = false → 통과
| 문자 하나를 MIME 타입 문자열 뒤에 붙이는 것만으로 isset() 검사가 "그런 키는 없다"고 판단해버립니다. 블록리스트가 애초에 설계된 의도(위험한 MIME 타입을 가진 파일을 막는 것)는 지켜지지 않고, 문자열이 정확히 일치하는지만 보는 구현의 허점이 그대로 우회로가 됩니다.
3단계 — 두 결함을 묶으면: 공격자가 지정한 이름과 내용의 PHP 파일이 그대로 저장된다
이 두 결함을 조합하면 공격자는 다음을 할 수 있습니다.
- Select 필드 값에 위조된 "업로드 필드 설정"을 끼워 넣어, 서버가 신뢰하는 허용 타입 목록 자체를 공격자 뜻대로 바꿔치기합니다.
- 설령 블록리스트가 별도로 남아있어도, MIME 타입 키에 |를 붙여 그 검사를 그대로 통과합니다.
- 파일 이름과 내용은 공격자가 완전히 통제합니다.
POST /wp-admin/admin-ajax.php
action=forminator_submit_form_custom-forms-5
file-upload-1[name]=shell.php
file-upload-1[content]=<?php system($_GET['cmd']); ?> select-1=<위조된 업로드 필드 설정이 인코딩된 값>
이 요청이 정상 처리되면, 업로드 디렉터리에 shell.php라는 이름의 PHP 파일이 공격자가 지정한 내용 그대로 생성됩니다.
4단계 — 기본 디렉터리는 .htaccess로 보호되지만, Custom Upload Storage는 그렇지 않다
여기서 끝이 아닙니다. 기본 설정에서는 Forminator의 업로드 디렉터리가 PHP 실행을 막는 .htaccess 파일로 보호되어 있어, 설령 .php 파일이 업로드되더라도 웹 서버가 이를 스크립트로 실행하지 않고 그대로 반환하는 경우가 많습니다. 그런데 관리자가 "Custom File Upload Storage root"를 별도로 지정해두면 상황이 달라집니다.
Wordfence는 이 부분을 다음과 같이 설명합니다. 이 .htaccess 파일은 커스텀 저장 경로가 "처음 필요해지는 시점", 즉 프론트엔드(비로그인) 제출 요청이 처리되는 도중에 생성됩니다. 그런데 이 프론트엔드 요청 경로에는 .htaccess 작성을 담당하는 워드프레스 헬퍼 함수 자체가 로드되어 있지 않습니다. 그 결과, 커스텀 업로드 경로에는 PHP 실행을 막는 보호 장치가 아예 만들어지지 않은 채로 파일이 쌓이게 됩니다.
curl "http://target/wp-content/uploads/forminator-custom/shell.php?cmd=id"
# → uid=33(www-data) gid=33(www-data) groups=33(www-data)
패치 — 설정은 서버가, 비교는 정규화해서
Forminator 1.56.2는 이 문제를 구조적으로 손봤습니다. 업로드 필드가 어떤 파일 타입을 허용할지는 더 이상 요청에 담긴 값으로 구성되지 않고, 서버에 저장된 폼 설정만을 신뢰하도록 바뀌었습니다. 아울러 위험 확장자/MIME 타입 블록리스트 비교도 단순 키 일치가 아니라, 입력을 정규화한 뒤 비교하도록 고쳐져 | 같은 구분자를 덧붙이는 방식의 우회가 통하지 않습니다.
이 취약점이 주는 교훈은 두 가지입니다. 첫째, "이 값은 서버가 이미 신뢰할 수 있는 설정값일 것"이라는 가정은 요청의 어느 자리에서든 깨질 수 있습니다. 업로드 필드를 직접 조작하지 않아도, 전혀 상관없어 보이는 다른 필드(Select)를 통해 그 설정 자체를 주입할 수 있다면 검증의 의미가 없어집니다. 둘째, 블록리스트/화이트리스트 비교는 입력을 정규화하지 않은 채 "문자열이 정확히 같은가"로만 판단하면, 공격자가 의미상 동일하지만 문자열상으로는 다른 변형을 얼마든지 만들어낼 수 있습니다.
대응 방안
- Forminator Forms를 1.56.2 이상으로 즉시 업데이트합니다.
- File Upload 필드와 Select 필드를 함께 쓰는 폼이 있는지 점검하고, 당장 업데이트가 어렵다면 해당 폼의 파일 업로드 필드를 일시적으로 비활성화하는 것을 고려합니다.
- "Custom File Upload Storage root"를 사용 중이라면, 해당 디렉터리에 PHP 실행을 막는 웹 서버 설정(Nginx location 규칙, Apache .htaccess)이 실제로 적용되어 있는지 플러그인 설정과 별개로 직접 확인합니다.
- 이미 노출되었을 가능성이 있다면, 기본 업로드 디렉터리와 커스텀 업로드 디렉터리 모두에서 낯선 .php/.phtml 파일이 있는지 점검하고, admin-ajax.php로 향하는 forminator_submit_form_* 액션 요청 중 비정상적으로 큰 select 파라미터 값이 있었는지 접근 로그를 확인합니다.
'CVE 취약점' 카테고리의 다른 글
| CVE-2026-107806 분석: 자물쇠와 열쇠를 같은 사람이 가지고 있다면, 그 서명은 아무것도 증명하지 못한다 (0) | 2026.10.19 |
|---|---|
| CVE-2026-53365 분석: "마지막 조각에만" 완료 추적을 붙였던 커널 vsock 제로카피의 함정 (0) | 2026.10.18 |
| CVE-2026-1357 분석: "복호화 실패"를 확인하지 않은 대가, WPvivid 백업 플러그인 RCE (0) | 2026.10.15 |
| [CVE-2026-49869] "접미사만 같으면 통과" — Kestra를 무너뜨린 한 줄의 비교 연산자 (0) | 2026.10.15 |
| CVE-2026-94545 분석: OG 이미지 생성기가 이스케이프를 빼먹었을 때 벌어지는 일 (0) | 2026.10.14 |