
ソフトウェア: 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);