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=fake4. 同じトークンでリクエストを送信
次に、同じ値の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は成立する
重要なのは、「トークンがあるか」ではなく「そのトークンを攻撃者が操作できるか」という視点です。
