CakePHPのフォームには、なぜ今もトークンが入っているのか――Sec-Fetch-Siteがある時代のCSRF対策
CakePHPでFormHelperを使ってフォームを作ると、自分では書いていないhiddenフィールドが追加されることがあります。
その一つがCSRFトークンです。
しかし、現代のブラウザーには、リクエストがどこから送られたかを知らせるSec-Fetch-Siteがあります。
送信元が分かるなら、トークンはもう古い仕組みなのでしょうか。
CakePHPのフォームを入口に、Cookie認証、ブラウザーの自動送信、Fetch Metadata、互換性、そしてセキュリティ機能の責務を整理してみましょう。
ログインしているのに、なぜトークンが必要なのか
管理画面の商品編集フォームを開いたずんだもんが、ブラウザーの開発者ツールを見つめていた。
💚ずんだもん
「この_csrfTokenというhiddenフィールド、ぼくは書いていないのだ」
🎀めたん
「CSRF Middlewareが有効な状態でFormHelperを使うと、CakePHPが自動的に追加します」
CakePHPの公式ドキュメントでも、FormHelperがフォームへCSRFトークンのhiddenフィールドを挿入すると説明されています。
💚ずんだもん
「でも、この管理画面にはログインしないと入れないのだ」
「ログインセッションがあるなら、本人の操作だと判断できるのではないのだ?」
🎀めたん
「そこでは、二つの判断が混ざっています」
「ログイン状態から分かるのは、誰としてアクセスしているかです。どこからその操作が始まったかまでは分かりません」
⚔️つるぎ
「ブラウザーは、送信先に合うCookieを自動的に付けます」
「利用者が攻撃者のページを開き、そのページから管理画面へ更新要求を送らされた場合でも、条件によっては正規のログインCookieが付いてしまいます」
CSRFではCookieそのものを盗む必要はありません。利用者のブラウザーに、保存済みのCookieを正規サイトへ送信させることが攻撃の出発点です。
💚ずんだもん
「攻撃者がぼくのCookieを知らなくても、ぼくのブラウザーが勝手に持っていってしまうのだ?」
⚔️つるぎ
「そうです。サーバーから見ると、Cookieだけは正規のものです」
「だから、ログイン済みであることだけを確認しても、その更新操作が本人の意図によるものかは判断できません」
🎀めたん
「役割を分けると、こうなります」
認証:誰としてアクセスしているか
CSRF対策:正規の画面やスクリプトへ渡されたトークンを返せる経路から、更新要求が送られたか
「同じリクエストを見ていますが、答えている問いが違います」
💚ずんだもん
「ログインは名札の確認で、CSRF対策は受付を通ってきたかの確認なのだ」
🎀めたん
「おおむね、その理解でよいでしょう」
ログインしていることと、その操作を本人が意図したことは同じではありません。
ではCakePHPは、その「受付を通った証拠」をどのように運んでいるのでしょうか。
CakePHP は正規の画面を通った証拠を確認する
🎀めたん
「まず、細かな設定ではなく、リクエストの流れを見ましょう」
画面を取得する
↓
CakePHPがトークンを用意する
↓
フォームまたはJavaScriptがトークンを返す
↓
CSRF Middlewareが検証する
↓
コントローラーへ進む
💚ずんだもん
「フォームのhiddenフィールドは、トークンを返すための入れ物なのだ?」
🎀めたん
「そうです」
「通常のHTMLフォームをFormHelperで作れば、CakePHPがその運搬を引き受けます。開発者が毎回hiddenフィールドを手書きする必要はありません」
CakePHP 5では、CookieにCSRFトークンを保持するCsrfProtectionMiddlewareと、セッションに保持するSessionCsrfProtectionMiddlewareが用意されています。両方を同時には使わず、どちらか一方を選びます。
💚ずんだもん
「では、fetch()でJSONを送るAPIなら、フォームを使わないからCSRF対策もなくなるのだ?」
🎀めたん
「なくなりません。トークンの運び方が変わるだけです」
「HTMLフォームならhiddenフィールド、JavaScriptから送るならX-CSRF-Tokenヘッダーを使えます」
CakePHPのCSRF Middlewareは、リクエスト本文だけでなくX-CSRF-Tokenヘッダーからもトークンを受け取れます。公式ドキュメントでも、JavaScript中心の画面やJSONエンドポイントではヘッダーが利用できると説明されています。
⚔️つるぎ
「JSONだから安全になるわけではありません」
「判断基準はデータ形式ではなく、ブラウザーが認証情報を自動送信するかどうかです」
💚ずんだもん
「フォームかJSONかではなく、Cookie認証のブラウザーから更新要求を送るかどうかなのだ」
🎀めたん
「そのとおりです」
「そして、この判断を各Controllerへ書き散らしてはいけません」
💚ずんだもん
「商品Controllerでも会員Controllerでも、それぞれトークンを確認するのは大変なのだ」
🎀めたん
「CSRFは個別の業務ルールではなく、HTTPリクエストを受け入れるための共通条件です」
「だから、Controllerへ処理が渡る前にMiddlewareで検証します」
「Controllerは、商品を変更できるか、注文を取り消せるかといった業務判断に集中します」
⚔️つるぎ
「入口で拒否できる要求を、業務処理の奥まで運ばない。責務だけでなく、失敗する位置も明確になります」
CSRFの判断は、業務処理へ入る前のHTTP境界で終わらせます。
ただし現代のブラウザーには、トークンとは別に、送信元を知らせる仕組みがあります。
Sec-Fetch-Site で送信元が分かるようになった
💚ずんだもん
「そこでSec-Fetch-Siteなのだ」
「ブラウザーが送信元を教えてくれるなら、もうトークンを削除できるのではないのだ?」
⚔️つるぎ
「Sec-Fetch-Siteは、リクエストを始めた場所と送信先との関係をブラウザーが示すヘッダーです」
「主な値は四つです」
same-origin:スキーム、ホスト、ポートが同じ
same-site:同じサイトに属する
cross-site:別のサイトから送られた
none:アドレスバーやブックマークなど、ブラウザーUIから始まった操作
MDNでは、Sec-Fetch-Siteを、要求元と要求先の関係をサーバーへ伝えるFetch Metadataヘッダーとして説明しています。
💚ずんだもん
「攻撃者のサイトから管理画面へPOSTされたら、cross-siteになるのだ?」
⚔️つるぎ
「対応ブラウザーから通常の方法で送られた要求なら、そう判断できます」
「そこでアプリケーションの早い段階に、次のような規則を置けます」
別サイトから来た更新要求
↓
業務処理へ入る前に拒否
Fetch Metadataは、サーバーがリクエストの文脈を見て、処理前に受け入れるかどうかを判断するための仕組みです。
🎀めたん
「CakePHPで採用するなら、これもHTTP境界の仕事です」
「Controllerの各アクションでSec-Fetch-Siteを読むのではなく、アプリケーション独自のMiddlewareへまとめます」
💚ずんだもん
「CakePHPには、Fetch Metadata専用のMiddlewareが最初からあるのだ?」
🎀めたん
「少なくとも現在のCakePHP 5.x標準ドキュメントには、Fetch Metadata専用Middlewareは示されていません」
「標準のCSRF Middlewareとは別に、必要なアプリケーションが独自に追加する機能として考えるのが適切です」
CakePHPの標準セキュリティ機能にはCSRF、CSP、HTTPS Enforcer、Security Headersなどが掲載されていますが、Fetch Metadata専用Middlewareは掲載されていません。したがって、導入する場合はPSR-15 Middlewareとしてアプリケーション側で設計する、という位置づけになります。
⚔️つるぎ
「Sec-Fetch-Siteは強力ですが、役割は『明らかに外部から来た要求を見つけること』です」
「それだけで、すべての利用経路が説明できるとは限りません」
Sec-Fetch-Siteは、明らかなクロスサイト要求を早く見つけるための判断材料です。
では、Sec-Fetch-Siteが送られてこなかった要求は、どう扱えばよいのでしょうか。
それでもヘッダーがない要求は残る
💚ずんだもん
「でも、Sec-Fetch-SiteはBaselineなのだ」
「それなら、もう全部のブラウザーで使えるのではないのだ?」
⚔️つるぎ
「Baselineの意味を広げすぎています」
「Baselineは、主要な現行ブラウザーで広く利用できる状態を示します。MDNでは、Sec-Fetch-Siteは2023年3月以降、主要ブラウザーで広く利用可能とされています」
💚ずんだもん
「広く使えるなら、十分ではないのだ?」
⚔️つるぎ
「一般消費者向けサイトなら、かなり高い割合をカバーできます」
「ただし、Baselineは次のものまで保証する制度ではありません」
更新されていない古いブラウザー
古いアプリ内WebView
独自実装された組み込みブラウザー
監視ツールや自動テスト
curlなどのHTTPクライアント
外部サービスから届くWebhook
「さらに、プロキシやゲートウェイの設定によってヘッダーが失われる可能性もあります」
OWASPも、古いブラウザーや組み込み環境ではFetch Metadataヘッダーがない場合があり、欠落時のフォールバック方針が必要だと説明しています。
🎀めたん
「ここで必要なのは、技術の新旧を決めることではありません」
「ヘッダーがない要求をどう扱うか、サービスの対応範囲を決めることです」
⚔️つるぎ
「選択肢は大きく二つあります」
ヘッダーがない要求を拒否する
防御方針を単純にできる
古いブラウザーや一部のクライアントは利用できなくなる
利用環境を限定できるサービスに向いている
ヘッダーがない要求を許可する
幅広いクライアントを維持できる
Fetch Metadataが働かない経路が残る
CSRFトークンやOrigin確認など、別の検証が必要になる
OWASPの整理でも、ヘッダー欠落時には、拒否する方式と、CSRFトークンなどへフォールバックする方式の両方が示されています。
💚ずんだもん
「ヘッダーがない要求を全部拒否すれば安全だけど、今まで使えていた利用者やツールが止まるかもしれないのだ」
⚔️つるぎ
「そうです」
「安全性だけでなく、利用者、運用ツール、外部連携を含めた契約変更になります」
🎀めたん
「通常のCakePHP業務アプリなら、標準のCSRF Middlewareを残すのが自然です」
「対応ブラウザーではFetch Metadataで明らかな攻撃を早期拒否し、ヘッダーがない経路でもCSRFトークンを検証できます」
「これなら、新しいブラウザー機能を利用しながら、既存の更新経路も同じ規則で守れます」
💚ずんだもん
「CSRFトークンは、古いブラウザーのためだけに残っているわけではないのだ」
「ヘッダーがある経路とない経路を、同じ更新ルールへ戻す役目があるのだ」
Baselineは、既存の防御を捨てる合図ではありません。対応範囲を判断するための基準です。
新しいヘッダーと従来のトークンは、どちらか一方を選ぶ競合機能ではありません。
トークンとヘッダーは競合しない
💚ずんだもん
「では、Sec-Fetch-SiteとCSRFトークンを両方使うのだ?」
「同じ確認を二回しているようにも見えるのだ」
🎀めたん
「同じ確認ではありません」
「それぞれが異なる問いを担当します」
Fetch Metadata:明らかなクロスサイト要求ではないか
CSRF Middleware:アプリケーションが配布したトークンを返しているか
Authentication:誰としてアクセスしているか
Authorization:その利用者が対象を操作できるか
業務ロジック:現在の状態で操作を許可できるか
⚔️つるぎ
「処理順を簡略化すると、こうなります」
Fetch Metadata
↓
CSRFトークン
↓
認証・認可
↓
業務処理
💚ずんだもん
「最初に外部サイトからの怪しい要求を落とす」
「次に正規のトークンを確認する」
「そのあとで、誰なのか、その人に権限があるのか、今その操作ができるのかを見るのだ」
🎀めたん
「きれいに分かれましたね」
「たとえばCSRFトークンが正しくても、一般社員が管理者専用データを削除してよいことにはなりません」
「権限があっても、発送済みの注文を無条件に未発送へ戻してよいとは限りません」
⚔️つるぎ
「もう一つ、same-siteにも注意が必要です」
「same-siteはsame-originより広く、兄弟関係にあるサブドメインからの要求を含みます」
「利用者が自由にコンテンツを置けるサブドメインや、管理の異なる古いサービスがあるなら、同じサイトだから安全とは断定できません」
OWASPも、兄弟サブドメインを信頼できない場合は、same-siteを更新要求に対して慎重に扱うよう示しています。
💚ずんだもん
「same-siteという名前だけを見て、社内だから全部安全、と決めてはいけないのだ」
⚔️つるぎ
「はい。ドメインの構成ではなく、実際に誰がその領域を管理しているかを見ます」
🎀めたん
「防御を一つの万能機能へ集約するのではなく、異なる判断を適切な境界へ置く」
「それが多層防御の設計です」
多層防御とは、同じ確認を何度も行うことではなく、異なる問いを異なる境界で確認することです。
CakePHPのフォームには、CSRFトークンとは別のトークンが追加される場合もあります。
CakePHPでは責務を重ねる
💚ずんだもん
「フォームを詳しく見ると、CSRFとは別に_Tokenという項目もあるのだ」
「これも同じ攻撃を防ぐトークンなのだ?」
🎀めたん
「それはFormProtectionComponentが使うトークンです」
「どちらもフォームへhiddenフィールドを追加するため似て見えますが、確認していることが違います」
機能確認することCSRF Middleware正規の操作経路へ配布されたトークンが返されたかFormProtectionComponent表示時と送信時でフォーム構造が変わっていないかValidation入力値の形式や範囲が正しいかAuthorization利用者がその操作を実行してよいか
💚ずんだもん
「フォーム構造が変わっていないか、というのはどういう意味なのだ?」
🎀めたん
「FormProtectionComponentは、FormHelperが作ったフォームの構造を記録します」
「そして送信時に、主に次の変更を検出します」
想定していないフィールドが追加された
本来あるフィールドが削除された
hiddenフィールドの値が変更された
フォームの送信先が変更された
CakePHPの公式ドキュメントでは、フォームのアクション、未知のフィールド、削除されたフィールド、hidden値の変更を検査対象として挙げています。
⚔️つるぎ
「CSRF対策は、要求が外部サイトから勝手に起動された可能性を扱います」
「Form Protectionは、正規画面として作られたフォームの構造が、送信時に書き換えられていないかを扱います」
💚ずんだもん
「外から送られたかと、中身の構造が変わっていないかは別の問題なのだ」
🎀めたん
「そのため、FormProtectionComponentはCSRF Middlewareの代わりにはなりません」
「ValidationやAuthorizationの代わりにもなりません」
💚ずんだもん
「では、APIならCSRF Middlewareを外してよいのだ?」
🎀めたん
「APIという名前だけでは判断できません」
「見るべきなのは、ブラウザーが認証情報を自動送信するかどうかです」
⚔️つるぎ
「たとえば、Cookie認証を使うSPAや、管理画面から呼び出すJSON APIでは、ブラウザーがCookieを自動送信します」
「そのためCSRF対策が必要です」
🎀めたん
「一方、AuthorizationヘッダーのBearer Tokenだけで認証し、Cookieやセッションを使わないステートレスAPIは、通常のCSRFとは前提が異なります」
CakePHPの公式ドキュメントも、Cookieやセッションを認証に使うステートフルなルートへCSRF Middlewareを適用し、Cookieを使わないステートレスなAPIでは必須ではないと説明しています。
⚔️つるぎ
「ただし、CSRF対策が不要だから無防備でよい、という意味ではありません」
「Bearer Tokenの管理、権限確認、レート制限、入力検証が必要です」
「Webhookなら、署名、共有秘密、送信時刻、再送への対応など、別の方法で送信者と要求の正当性を確認します」
💚ずんだもん
「URLに/apiと書いてあるかではなく、認証情報がどう運ばれるかを見るのだ」
🎀めたん
「その判断なら、画面の名前やデータ形式が変わっても通用します」
「CakePHPのCSRFトークンは、古いブラウザー向けに仕方なく残された部品ではありません」
「ブラウザーや画面実装が変わっても、更新要求を同じ契約で検証するための境界です」
セキュリティ機能を残すかどうかは、技術の新しさではなく、どの利用経路を支えるかで決まります。
まとめ
💚ずんだもん
「最初は、Sec-Fetch-SiteがあるならCSRFトークンはいらないと思っていたのだ」
⚔️つるぎ
「Fetch Metadataは、現代のブラウザーが持つ有力な判断材料です」
「明らかなクロスサイト要求を、アプリケーションの入口で拒否できます」
🎀めたん
「しかし、ヘッダーがない要求を許可するなら、別の検証経路が必要です」
「CakePHPのCSRF Middlewareを残せば、HTMLフォームでも、JavaScriptから送るJSONでも、共通のトークン検証を適用できます」
💚ずんだもん
「認証、Fetch Metadata、CSRF、Form Protection、認可、業務ルールは、それぞれ違う問いを担当するのだ」
⚔️つるぎ
「新しい仕組みを追加することと、既存の仕組みを廃止することは別の判断です」
「廃止するには、新しい仕組みが存在するだけでなく、守るべきすべての経路を代替できる必要があります」
🎀めたん
「だから結論は、これです」
CSRFトークンは古い仕組みだから残っているのではない。ヘッダーが存在しない経路まで、同じルールで守るために残っている。
フォームに自動追加された小さなhiddenフィールドは、過去の名残ではありません。
画面、JavaScript、ブラウザー、業務処理の境界をつなぎ、更新要求を受け入れる条件を統一するための部品なのです。
