WordPressで考えるCSRFトークンの欠陥│ダブルサブミット(二重送信)はなぜ危険なのか

CSRF対策としてトークンを実装していても、その検証方法によっては簡単に突破されることがあります。

この記事では、Web Security Academyのラボ「CSRF where token is duplicated in cookie」をもとに、

  • Cookieとリクエストを比較するだけのCSRF対策の問題点
  • WordPressで起こり得る実装ミス
  • 攻撃の成立条件と対策

を解説します。

CSRF where token is duplicated in cookieラボの概要

このラボでは、メールアドレス変更機能にCSRF対策が実装されています。

しかし、その中身は以下のようなものです。

  • CSRFトークンがCookieにも保存されている
  • POSTパラメータにも同じトークンが送信される
  • サーバーは両者が一致しているかだけを確認する

一見すると対策されているように見えますが、実際には重大な問題があります。


攻撃の流れ|Cookie注入によるCSRF成立の仕組み

この脆弱性を利用した攻撃は、以下の流れで成立します。

1. 正常なリクエストの構造を確認

メール変更時のリクエストは次のようになっています。

  • Cookie:csrf=XXXX
  • POST:csrf=XXXX

サーバーはこの2つが一致しているかを確認しています。

2. Cookieを書き換える手段を探す

このラボでは、検索機能に以下の問題があります。

  • 検索キーワードがそのままSet-Cookieに反映される
  • CSRF保護がされていない

これにより、ユーザー入力を使って任意のCookieを設定できます。

3. CSRFトークンを偽造してCookieに注入

攻撃者は以下のようなリクエストを作成します。

/?search=test%0d%0aSet-Cookie: csrf=fake; SameSite=None

これにより、被害者のブラウザに以下のCookieが設定されます。

csrf=fake

4. 同じトークンでリクエストを送信

次に、同じ値のCSRFトークンを使ってメール変更リクエストを送信します。

  • Cookie:csrf=fake
  • POST:csrf=fake

5. サーバーの判定

サーバーは以下を確認します。

  • CookieとPOSTが一致している → OK

その結果、攻撃が成立します。


なぜCSRF対策が突破されるのか

この脆弱性の本質はシンプルです。

サーバーがトークンを管理していない

本来のCSRF対策は以下です。

  • サーバーがトークンを発行する
  • サーバーがそのトークンを保持する
  • 送信されたトークンと照合する

しかしこの実装では、

  • サーバーはトークンを保持していない
  • クライアント同士(CookieとPOST)を比較しているだけ

つまり、

正しいかではなく、一致しているかしか見ていません。

Cookieが攻撃者に操作されている

さらに問題なのは、

  • Cookieがユーザー入力経由で書き換え可能
  • SameSite制御も回避されている

という点です。

その結果、攻撃者が任意のトークンを自由に作れる状態になります。


攻撃者視点で見ると

攻撃者は次のように考えます。

1. トークンは突破する必要があるか?

いいえ、作ればいい。

2. そのトークンはどこで使われているか?

  • Cookie
  • POSTパラメータ

3. どちらか操作できるか?

  • POSTは自由に作れる
  • Cookieも検索機能で書き換えられる

結論

両方自分で用意できるなら、防御は存在しない。


WordPressで起こり得る実装ミス

WordPressでは nonce(CSRFトークン)を使うのが一般的ですが、以下のような実装ミスが起こり得ます。

例1:nonceをCookieと比較しているだけの実装

独自実装でたまにあるパターンです。

$cookie_nonce = $_COOKIE['csrf'];
$post_nonce = $_POST['csrf'];if ($cookie_nonce === $post_nonce) {
// 更新処理
}

一見するとトークンをチェックしているように見えますが、

  • サーバー側でトークンを管理していない
  • CookieとPOSTの一致しか見ていない

という状態です。

この場合、Cookieを書き換えられたり、POSTも自由に送れるため、攻撃者が任意の値で一致させることができます。

例2:WordPressのnonceを使わず独自トークンを実装

WordPressには wp_verify_nonce() がありますが、それを使わずに独自実装してしまうケースです。

// トークン生成
$token = md5(time());
setcookie('csrf', $token);// フォームに埋め込み
echo '<input type="hidden" name="csrf" value="'.$token.'">';

検証側

if ($_COOKIE['csrf'] === $_POST['csrf']) {
// OK
}

この実装では、

  • トークンの正当性が保証されない
  • Cookie依存のため操作可能

という問題があります。

例3:検索機能や設定機能でCookieを書き換えられる

例えば、検索キーワードや設定値をCookieに保存する処理です。

setcookie('last_search', $_GET['search']);

このような実装があると、

  • 入力値がそのままCookieに入る
  • ヘッダインジェクション(CRLF)が可能な場合、任意のCookieを追加できる

結果として、

Set-Cookie: csrf=fake

のようにCSRFトークンを書き換えられる可能性があります。

例4:Ajax処理だけ検証が甘い

WordPressではAjax(admin-ajax.php)を使うことが多いですが、

// 通常フォーム
wp_verify_nonce($_POST['_wpnonce'], 'update_email');// Ajax側
if ($_COOKIE['csrf'] === $_POST['csrf']) {
update_user_meta(...);
}

このように、通常処理は安全だが、Ajaxだけ独自実装になっているケースがあります。

この場合、Ajax経由でCSRFが成立します。

例5:nonceはあるが検証していない

見た目だけ対策しているパターンです。

<input type="hidden" name="_wpnonce" value="<?php echo wp_create_nonce('update'); ?>">

しかしサーバー側で

// 検証していない
update_user_meta(...);

この場合、nonceは存在するが意味がなく、CSRFは普通に成立します。

共通する問題点

これらに共通しているのは以下です。

  • 複数の経路で検証がバラバラ
  • サーバー側で正しい値を持っていない
  • クライアントの値をそのまま信頼している

CSRF対策として正しい実装とは

1. サーバー側でトークンを管理する

  • セッションに紐づける
  • 使い回しを防ぐ
  • ワンタイム化する

2. Cookieを信頼しない

  • Cookieはクライアント側のデータ
  • 攻撃者が操作できる前提で扱う

3. 入力値をCookieに反映しない

  • 特にヘッダへの反映は危険
  • CRLFインジェクション対策を行う

4. SameSite属性に依存しすぎない

  • SameSiteは補助的な防御
  • 単体では不十分

セキュリティ診断で確認すべきポイント

CSRF診断では、以下を確認すると見つけやすくなります。

1. トークンの検証方法

  • サーバー側で保持されているか
  • 単なる一致チェックになっていないか

2. Cookieの扱い

  • トークンがCookieに保存されていないか
  • Cookie値が攻撃者に操作可能でないか

3. Cookie設定機能の有無

  • 検索機能
  • トラッキング機能
  • 設定保存機能

これらがSet-Cookieに影響していないか確認する。

4. 入力値のヘッダ反映

  • CRLFインジェクションの有無
  • レスポンスヘッダへの反映箇所

5. SameSiteの挙動

  • クロスサイトでCookieが送信されるか
  • None指定の影響

まとめ|一致しているだけの検証は防御にならない

  • CookieとPOSTの一致だけを見るCSRF対策は不十分
  • サーバーがトークンを管理していない場合、防御は成立しない
  • Cookieが操作可能な時点で、CSRFは成立する

重要なのは、「トークンがあるか」ではなく「そのトークンを攻撃者が操作できるか」という視点です。