CSRF対策としてトークンを実装しているから安全、と思っていませんか。
実際には、トークンの実装方法によっては簡単に突破されるケースがあります。
本記事ではWeb Security Academyのラボ「CSRF where token validation depends on token being present」をもとに、トークン未送信でも成立するCSRFを解説し、WordPressに置き換えて考えます。
CSRF where token validation depends on token being presentラボ概要
このラボでは、メールアドレス変更機能にCSRF脆弱性が存在します。
一見するとCSRFトークンによる対策が実装されていますが、実際には次の問題があります。
- トークンが送信された場合のみ検証される
- トークンが未送信の場合は検証がスキップされる
そのため、攻撃者はトークンを偽造する必要がなく、トークン自体を送らないリクエストを作成することでCSRFを成立させることができます。
目的
Exploit Serverに用意したHTMLページを利用し、そのページを閲覧したユーザーのメールアドレスを、攻撃者の指定したものに変更させます。
攻撃のポイント
- CSRFトークンは存在するが必須ではない
- トークン未送信で検証を回避できる
- ブラウザが自動でCookieを送信する
このラボで学べること
- CSRF対策は「トークンの有無」では不十分であること
- トークンは必ず検証される設計でなければならないこと
- 実装ミスにより対策が無効化される典型例
CSRF対策の基本
CSRFは、ユーザーが意図しないリクエストを送らされる攻撃です。
一般的な対策は次の通りです。
- フォームにCSRFトークンを埋め込む
- サーバー側でトークンを検証する
これにより、正規ユーザーからのリクエストかどうかを判別します。
トークンがあっても防げないケースとは
今回のラボでは、次のような実装上の問題がありました。
トークンが存在する場合のみ検証される実装では、トークンが未送信の場合に検証自体が行われません。
脆弱な実装の例
if (isset($_POST['csrf'])) {
validate_csrf($_POST['csrf']);
}// メール変更処理
change_email($_POST['email']);一見するとCSRF対策が実装されているように見えますが、重要な欠陥があります。
問題点
このコードの挙動は次の通りです。
- トークンがある → 検証する
- トークンがない → そのまま処理する
つまり、攻撃者はトークンを偽造する必要がなく、トークンを送らなければ検証を回避できる状態です。
CSRF攻撃の流れ|メール変更が実行されるまで
- 被害者がログイン済みの状態になる
セッションCookieがブラウザに保存されている。 - 攻撃者のページを開く
被害者が攻撃用ページを閲覧する。 - フォームが自動送信される
JavaScriptによりリクエストが自動で送信される。 - Cookieが自動付与される
ブラウザはセッションCookieを付けてリクエストを送信する。 - サーバーがリクエストを受理する
トークンが未送信でも検証されないため、そのまま処理が実行される。 - メールアドレスが変更される
被害者の意図に反して変更が完了する。
実際のペイロード例
<form action="https://example.com/email/change" method="POST">
<input type="hidden" name="email" value="attacker@example.com">
</form>
<script>
document.forms[0].submit();
</script>ポイントは、CSRFトークンのパラメータを一切送らないことです。
なぜCSRFが成立するのか
この攻撃が成立する理由は、CSRF対策の設計にあります。
トークンが必須になっていない
トークンが存在する場合のみ検証する実装では、未送信時に検証がスキップされます。
そのため、攻撃者はトークンを偽造する必要がありません。
ブラウザが認証情報を自動送信する
ブラウザは同一サイトへのリクエストに対して、Cookieを自動的に付与します。
その結果、サーバーはリクエストを正規ユーザーのものと誤認します。
検証が条件によってスキップされる設計
CSRF対策は本来、すべてのリクエストで必ず実行されるべき処理です。
しかし実際のコードでは、次のように条件によって処理を変える書き方がされることがあります。
if (isset($_POST['csrf'])) {
validate_csrf($_POST['csrf']);
}このコードは、
- csrfがある → 検証する
- csrfがない → 検証しない
という動きになります。
このように、入力の有無によって処理を分ける書き方を条件分岐といいます。
何が問題か
このような実装では、トークンが送られなかった場合に検証がスキップされます。
つまり、攻撃者はトークンを偽造する必要がなく、トークン自体を送らないだけで検証を回避できる状態になります。
実装の不整合が発生しやすい
以下のような状況で、この問題は発生しやすくなります。
- Ajax対応で処理が分岐する
- 共通処理の使い回し
- 後からCSRF対策を追加
- 一時的な例外対応が残る
その結果、一部の経路で検証が行われない状態になります。
WordPressで起こるCSRF実装ミス|nonceの落とし穴
WordPressでは nonce を使ってCSRF対策を行います。
しかし、以下のような実装では同じ問題が発生します。
if (isset($_POST['_wpnonce'])) {
wp_verify_nonce($_POST['_wpnonce'], 'update_email');
}// 更新処理この場合、nonceが未送信でも処理が通ってしまいます。
正しいCSRF対策|トークンの存在と正当性を検証する
CSRF対策では、トークンの存在と正当性の両方を検証する必要があります。
if (!isset($_POST['_wpnonce'])) {
exit('Invalid request');
}if (!wp_verify_nonce($_POST['_wpnonce'], 'update_email')) {
exit('Invalid nonce');
}重要なのは、トークンが存在しない時点で拒否することです。
セキュリティ診断でのチェックポイント
CSRF対策を確認する際は、次の点をチェックします。
- トークンが未送信でも処理が通らないか
- すべてのエンドポイントで検証されているか
- Ajaxや別ルートで例外がないか
- GETリクエストで状態変更ができないか
トークンの有無ではなく、必須になっているかを確認することが重要です。
まとめ|トークンは必須でなければ意味がない
CSRF対策で重要なのは次の2点です。
- トークンが存在すること
- トークンが正しいこと
どちらか一方でも欠けると、防御は成立しません。
CSRF対策はトークンを実装することではなく、すべてのリクエストで必ず検証される設計にすることが重要です。
トークンが任意扱いになっている時点で、その対策は機能していません。
