
개요
| 항목 | 내용 |
| CVE ID | CVE-2026-12227 |
| CVSS 3.1 | 9.8 (Critical) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-98 (PHP Program에서 Include/Require 대상 파일명에 대한 부적절한 통제) |
| 영향받는 제품 | WordPress 플러그인 Visual Composer Website Builder ≤ 45.16.0 |
| 패치 버전 | 45.16.1 |
| 취약 파일 | visualcomposer/Modules/Editors/Settings/PageTemplatesController.php |
| 취약 함수 | viewPageTemplate() |
이 취약점의 핵심은 "검증 이후에 값을 변형한다"는 순서 실수 하나입니다. 인증 절차가 아예 없고, HTTP 요청 하나로 서버의 임의 PHP 파일을 include시킬 수 있어서 CVSS가 9.8까지 올라갔습니다.
취약점이 발생하는 위치
Visual Composer는 커스텀 레이아웃 페이지를 렌더링할 때 WordPress 코어가 제공하는 template_include 필터에 자체 콜백을 등록해 둡니다. 이 필터는 WordPress가 "이 요청에 어떤 템플릿 파일을 보여줄지" 최종 결정하는 지점이라서, 여기에 걸리는 코드는 로그인 여부와 무관하게 모든 방문자의 요청마다 실행됩니다.

우선순위(priority) 11에, 별도의 권한 체크(current_user_can() 같은) 없이 걸려 있다는 게 1차 문제입니다. 로그인한 관리자든 로그아웃한 익명 방문자든 이 함수를 그대로 통과합니다.
소스코드 분석 — "검증 후 변형(validate-then-mutate)" 구조
공개된 연구 자료를 재구성하면 viewPageTemplate() 내부 로직은 대략 이런 흐름입니다.

한 줄씩 쉽게 풀어보겠습니다.
① getCurrentTemplateLayout() 사용자가 URL 쿼리스트링으로 보낸 vcv-template, vcv-template-type 값을 그대로 가져옵니다. 이 시점에서 $current['value']는 공격자가 완전히 통제하는 문자열입니다.
② validate_file() — 검증 WordPress 코어 함수 validate_file()은 문자열 안에 리터럴로 ..가 들어 있는지, 또는 절대경로(/로 시작)인지를 검사해서 0이 아닌 값을 반환하면 "위험한 경로"로 간주합니다. 즉 ../../etc/passwd 같은 값은 여기서 바로 걸러집니다.
문제는 이 검사가 가공되기 전의 원본 문자열에 대해서만 이루어진다는 점입니다. 아직 ..가 문자열 안에 실제로 나타나지 않는 형태로 값을 보내면 이 검사를 그냥 통과해버립니다.
③ str_replace('theme:', '', ...) — 변형 템플릿 타입이 vc-custom-layout이고 값에 theme:라는 문자열이 들어 있으면, 그 theme:를 전부 제거합니다. 원래 의도는 "테마 폴더 기준 상대경로"임을 나타내는 접두사(prefix)를 정리하려는 것으로 보이지만, 검증이 끝난 뒤에 문자열을 다시 조작한다는 게 치명적입니다.
④ locate_template() — 싱크 WordPress 코어 함수로, 넘겨받은 경로의 파일을 찾아 실제로 include합니다. PHP 파일이면 당연히 그 안의 코드가 그대로 실행됩니다.
왜 이게 뚫리는가 — 조각난 페이로드
공격자가 다음 값을 보낸다고 가정해봅시다.
theme:.theme:./.theme:./.theme:./wp-links-opml.php
- ② 검증 시점: 이 문자열 어디에도 ..라는 리터럴 조합이 존재하지 않습니다. theme:., .theme:./ 처럼 theme: 문자열이 .과 . 사이에 끼어들어가 있어서, validate_file() 눈에는 안전한 문자열로 보입니다. → 통과
- ③ 변형 시점: str_replace('theme:', '', ...)가 모든 theme:를 제거합니다. 그러면
theme:.theme:./.theme:./.theme:./wp-links-opml.php → (theme: 전부 삭제)
..../.../.../wp-links-opml.php → (남은 점과 슬래시가 재배열) ../../../../wp-links-opml.php
검증받지 않은 상태에서 디렉터리 트래버설 시퀀스가 새로 조립됩니다.
- ④ 싱크: locate_template()는 이 조립된 상대경로를 그대로 받아 워드프레스 루트 상위 디렉터리로 거슬러 올라가는 파일을 include합니다.
한 문장으로 요약하면, "검증은 재료 상태에서 하고, 조립은 검증 뒤에 한다"는 순서가 뒤바뀌어 있어서 생기는 전형적인 TOCTOU 류의 논리 결함입니다.
왜 이게 뚫리는가 — 조각난 페이로드
위 5단계 다이어그램은 요청 하나가 서버 내부에서 어떻게 처리되는지를 순서대로 보여줍니다.

- 1단계(회색): 공격자가 아무 인증 없이 vcv-template 파라미터에 조각난 페이로드를 담아 요청을 보냅니다.
- 2단계(파랑): 이 요청이 WordPress의 template_include 필터를 거치는데, 이 필터 콜백에는 권한 체크 코드가 아예 없습니다. 그래서 익명 방문자의 요청도 그대로 처리됩니다.
- 3단계(초록 — 안전해 보이는 구간): validate_file()이 문자열을 검사합니다. 이 시점의 문자열에는 ..가 리터럴로 나타나지 않기 때문에 "안전하다"고 판정하고 통과시킵니다. 여기가 함정입니다 — 검증 자체는 정상 동작했지만, 검증 대상이 "최종적으로 쓰일 값"이 아니었습니다.
- 4단계(주황 — 진짜 문제): 검증이 끝난 뒤에 str_replace()가 theme: 문자열을 제거합니다. 조각조각 흩어져 있던 점(.)과 슬래시(/)가 이 제거 과정에서 하나로 뭉쳐지면서 ../../../../ 같은 트래버설 경로가 사후에 새로 만들어집니다.
- 5단계(빨강 — 실제 피해): 이렇게 만들어진, 한 번도 검증받은 적 없는 최종 경로가 locate_template()에 그대로 전달되어 파일이 include됩니다. PHP 파일이면 그 자리에서 실행되므로 서버에 이미 올라와 있는 "안전한 척하는" 업로드 파일(예: PHP 코드가 숨겨진 이미지) 등을 include시켜 RCE로 이어질 수 있습니다.
하단에 적어둔 한 문장 — "검증은 가공 전 문자열을, 싱크는 가공 후 문자열을 사용한다" — 이게 이 취약점 전체를 관통하는 핵심입니다. 검증 로직 자체는 틀리지 않았는데, 검증과 사용 사이에 문자열이 한 번 더 바뀐다는 걸 놓친 게 원인이었습니다.