見出し画像

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、ブラウザー、業務処理の境界をつなぎ、更新要求を受け入れる条件を統一するための部品なのです。

いいなと思ったら応援しよう!