
CVE-2026-97286에 대한 기술 권고 및 개념 증명으로, WordPress Strong Testimonials 플러그인의 인증된 저장형 XSS 취약점이며, 공격 체인, PoC 및 패치 분석을 포함합니다.
인증된 Contributor는 WordPress Strong Testimonials 플러그인에 공격자가 제어하는 플랫폼 사진 메타데이터를 저장할 수 있습니다. 버전 3.3.12 이전에는 플러그인이 저장된 값을 이미지 src 속성 내부에 컨텍스트 이스케이프 없이 렌더링했습니다. 관리자가 조작된 추천사와 이를 표시하는 게시물을 게시한 후, 해당 게시물을 여는 방문자(인증되지 않은 방문자 포함)에게 페이로드가 실행됩니다.
이 연구는 승인된 로컬 랩에서 수행되었으며 공개 릴리스 전에 Patchstack을 통해 공개되었습니다.
| 필드 | 값 |
|---|---|
| CVE | CVE-2026-97286 |
| 제품 | Strong Testimonials |
| 공급업체 | WP Chill |
| 플러그인 슬러그 | strong-testimonials |
| 취약점 | 인증된 저장형 크로스 사이트 스크립팅 |
| 영향받는 버전 | <= 3.3.11 |
| 패치된 버전 | 3.3.12 |
| 필요한 권한 | Contributor 이상 |
| CWE | CWE-79: 웹 페이지 생성 중 입력의 부적절한 중화 |
| CVSS v3.1 | 6.5 Medium — CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:L |
| 연구자 | Rully Miftahur Rozaq |
| 보고일 | 2026년 9월 16일 |
| 공개 공개일 | 2026년 9월 30일 |
할당된 CVSS v3.1 기본 심각도가 Medium임에도 불구하고 Patchstack은 이 권고를 낮은 우선순위로 분류합니다.
추천사 편집기는 custom 요청 배열을 받아들이고 제출된 모든 키를 게시물 메타데이터로 저장합니다. 구성된 사용자 정의 필드 정의에 없는 키는 sanitize_text_field()로 넘어갑니다. 이를 통해 Contributor는 자신이 소유한 추천사를 편집하는 동안 platform 및 platform_user_photo와 같은 플러그인 예약 메타데이터를 제출할 수 있습니다.
지원되는 소셜 플랫폼의 경우, 플러그인은 platform_user_photo를 검색하고 URL이 HTTP 200 응답을 반환하는지 확인한 다음, 원래 저장된 문자열을 <img src="https://raw.githubusercontent.com/rully2212/cve-2026-97286/main/..."> 속성에 삽입합니다. HTTP 상태 검증은 문자열을 HTML 속성에 안전하게 만들지 않습니다. URL 조각의 따옴표는 src를 종료하고 이벤트 핸들러를 도입할 수 있습니다.
검증된 공격 체인은 다음과 같습니다:
주입은 인증이 필요합니다. 이미 게시된 페이로드를 트리거하는 방문자는 인증할 필요가 없습니다.
성공적인 익스플로잇은 취약한 썸네일 경로를 통해 조작된 추천사를 렌더링하는 페이지에서 영향을 받는 WordPress 오리진 내에 지속적인 JavaScript 실행을 제공합니다. 코드는 방문자의 브라우저 권한 및 WordPress 세션(있는 경우)으로 실행됩니다.
재현된 경로는 관리자의 게시를 필요로 하며, 이는 CVSS UI:R 지표를 설명합니다. 이 연구는 인증되지 않은 주입, 모든 사이트 페이지에 걸친 자동 실행, 권한 상승, 쿠키 탈취 또는 전체 사이트 침해를 입증하지 않았습니다.
개념 증명은 다음 구성으로 재현되었습니다:
| 구성 요소 | 구성 |
|---|---|
| WordPress | 7.1 |
| Strong Testimonials | 3.3.7, 무료 에디션 |
| 테마 | Twenty Twenty-Five |
| 표시 템플릿 | default-theme |
| 썸네일 | 활성화됨 |
| PHP / Apache | PHP 8.3.33 / Apache 2.4.68 |
| 데이터베이스 | MariaDB 11.4 |
| 공격자 역할 | Contributor |
| 공격자 권한 | edit_posts=true, publish_posts=false, unfiltered_html=false |
버전 3.3.7은 랩에서 사용된 정확한 버전이었습니다. 공개 CVE 기록은 3.3.11까지의 모든 버전을 영향을 받는 버전으로, 버전 3.3.12를 수정된 버전으로 식별합니다.
admin/class-strong-testimonials-post-editor.php에서 save_details()는 제출된 custom 배열의 모든 항목을 순회합니다. 인식되지 않는 키는 일반적인 정리(sanitization) 분기에 도달한 후 update_post_meta()로 저장됩니다:
$custom = $_POST['custom'];
foreach ( $custom as $key => $value ) {
// ...
$sanitized_value = sanitize_text_field( $value );
update_post_meta( $post_id, $key, $sanitized_value );
}
실제 핸들러는 nonce 검사와 필드별 정리(sanitization)를 수행합니다. 이 취약점은 해당 검사를 우회하거나 다른 사용자의 추천사를 편집하는 데 의존하지 않습니다. Contributor는 자신의 추천사를 정당하게 편집하지만 예약된 키인 platform과 platform_user_photo를 제공합니다.
취약한 버전에서 includes/functions-image.php는 platform_user_photo를 읽고, wpmtst_is_valid_image_url()을 통해 HTTP 상태 검사를 수행한 뒤, 원본 문자열을 HTML 속성에 삽입합니다:
$platform_user_photo = get_post_meta( $post_id, 'platform_user_photo', true );
if ( ! $platform_user_photo || ! wpmtst_is_valid_image_url( $platform_user_photo ) ) {
return $img;
}
return sprintf(
'<img src="https://raw.githubusercontent.com/rully2212/cve-2026-97286/main/%25s" %s %s/>',
$platform_user_photo,
$width ? "width='{$width}'" : '',
$height ? "height='{$height}'" : ''
);
URL의 fragment 부분은 HTTP 서버로 전송되지 않지만, 이후 PHP가 렌더링하는 문자열에는 남아 있습니다. 따라서 서버는 HTTP 200을 반환하는 무해한 URL을 보는 반면, 브라우저는 속성 탈출(attribute-breakout) 시퀀스가 포함된 값을 수신하게 됩니다.
default-theme 템플릿은 wpmtst_get_thumbnail()을 호출하고 반환된 이미지 마크업을 직접 출력합니다:
<figure class="wp-block-image wpmtst-testimonial-avatar">
<?php echo $wpmtst_avatar_html; ?>
</figure>
템플릿은 헬퍼가 이미 안전한 HTML을 생성했다고 가정합니다. 영향을 받는 버전에서 플랫폼 사진 헬퍼는 src 컨텍스트에 대한 URL을 이스케이프하지 않았습니다.
소유하고 있거나 테스트 권한을 명시적으로 부여받은 시스템에서만 이 문제를 재현하십시오.
<= 3.3.11.default-theme를 사용하는 Strong Testimonials Display 뷰.Contributor로 로그인하여 새 testimonial 초안을 생성합니다.
가로채기 프록시를 사용하여 정당한 POST /wp-admin/post.php 저장 요청을 캡처합니다. 전체 폼 본문, 세션 쿠키, 새 WordPress nonce, 플러그인 nonce, 그리고 Contributor가 소유한 testimonial의 ID를 보관합니다.
폼 본문에 다음 매개변수를 추가합니다:
custom[platform]=facebook
custom[platform_user_photo]=http://wordpress/index.php?rest_route=/#" onerror="alert(1)" x="
URL 인코딩된 폼 조각:
&custom%5Bplatform%5D=facebook&custom%5Bplatform_user_photo%5D=http%3A%2F%2Fwordpress%2Findex.php%3Frest_route%3D%2F%23%22%20onerror%3D%22alert%281%29%22%20x%3D%22
wordpress 호스트명은 로컬 랩에서 사용되는 Docker 서비스 이름입니다. 다른 승인된 환경에서는 WordPress에서 접근 가능하고 HTTP 200을 반환하는 연구자가 제어하는 테스트 엔드포인트로 교체하십시오. 제3자 또는 민감한 내부 서비스를 대상으로 하지 마십시오.
수정된 저장 요청을 전송하고 추천사를 검토를 위해 제출합니다.
Shortcode 블록을 포함하는 일반 WordPress 게시물을 작성합니다. 예시 ID를 테스트 설치의 Display view ID와 조작된 추천사 ID로 교체하십시오:
[testimonial_view id="2" post_ids="22" thumbnail="1"]

게시물을 검토용으로 제출합니다. 관리자로서 조작된 추천사와 트리거 게시물을 모두 게시합니다.
WordPress 로그인 없이 별도의 브라우저 또는 시크릿 창에서 게시된 트리거 게시물을 엽니다.
저장된 값은 게시물 메타데이터에서 직접 확인되었습니다:

프런트엔드 응답에는 다음과 동등한 이미지 요소가 포함되어 있었습니다:
<img src="http://wordpress/index.php?rest_route=/" onerror="alert(1)" x="" width="100" height="100">
인증되지 않은 방문자에 대해 실행된 페이로드:

재사용할 수 없는, 정제된 요청 템플릿이 poc/request-template.txt에 포함되어 있습니다. 이 템플릿은 재사용 가능한 인증 및 nonce 값을 의도적으로 생략합니다.
Strong Testimonials 3.3.12는 취약한 싱크에서 컨텍스트 기반 URL 이스케이프를 적용합니다:
-return sprintf( '<img src="https://raw.githubusercontent.com/rully2212/cve-2026-97286/main/%25s" %s %s/>', $platform_user_photo, $width ? "width='{$width}'" : '', $height ? "height='{$height}'" : '' );
+return sprintf( '<img src="https://raw.githubusercontent.com/rully2212/cve-2026-97286/main/%25s" %s %s/>', esc_url( $platform_user_photo ), $width ? "width='{$width}'" : '', $height ? "height='{$height}'" : '' );
수정 사항은 3.3.12 태그가 지정된 소스에서 검토할 수 있습니다. 취약한 구현은 3.3.11에서 여전히 확인할 수 있습니다.
사이트 소유자는 Strong Testimonials를 3.3.12 이상 버전으로 업데이트해야 합니다.
심층 방어를 위해 유지관리자는 다음 사항도 준수해야 합니다:
custom 키를 저장하는 대신, 후기 편집기에서 허용하는 메타데이터 키를 허용 목록으로 관리합니다.esc_url_raw() 또는 이에 상응하는 URL 정화 함수를 사용하여 허용된 URL 필드를 검증합니다.esc_url()과 일반 속성에 대한 esc_attr()이 포함됩니다.