- ブラウザURL入力今ここ
- DNS名前→IP
- ルータ経路選択
- ロードバランサ振り分け
- Nginx受け口
- アプリ処理今ここ
- DB保存
前回は、SameSite の判定単位が site であることを見ました。今回は、サーバが自分のページから来た要求を見分けるために何を照合するかを見ます。
まず結論
このコースの芯は「ブラウザが確かめられるのは『どこから来たか』までで、『本人が望んだか』はサーバが自分で確かめるしかない」です。CSRF の防御はこの境目にあり、サーバは Cookie ではなく、別サイトのページには作れない信号を要求の中から探して照合します。
信号は2種類あります。別オリジンからは読めない CSRF トークンと、スクリプトから書き換えられない Sec-Fetch-Site・Origin ヘッダです。どちらもブラウザの中で動く別サイトのページにだけ効く仕組みで、OWASP はヘッダで判定する場合も旧ブラウザ向けのフォールバックを必須としています。
自前のサーバ b.test にログイン済みのブラウザで、(1)b.test 自身のフォームと (2)別サイト a.test のフォームから、同じ b.test/update へ POST しました。セッション Cookie(SameSite=None)はどうなっていたでしょうか。
当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。
Cookie では区別できないので、別の信号を探す
SameSite などで制限しない限り、トップレベルのフォーム送信では、Cookie は要求を始めたページがどこであっても宛先のサイトの Cookie として付いていきます。サーバから見ると、本人が自分の画面で押したボタンと、別サイトのページが勝手に送らせたフォームは、同じ Cookie を持った同じ形の要求です。
ブラウザは「どこから来たか」を教えてくれます。しかし「本人が望んだか」は教えてくれません。そこでサーバは、別サイトのページには作れないものを要求の中に要求し、自分で照合します。作れない理由は2つあります。
1つは読めないこと。CSRF トークンはサーバが発行して自分の画面に埋めた乱数です。別オリジンのページは b.test に要求を送ることはできても、その応答を読めないので、画面に埋まった値を知りようがありません。OWASP は CSRF tokens prevent CSRF because without a CSRF token, an attacker cannot create valid requests to the backend server. と書きます。
もう1つは書けないこと。Fetch Standard §2.2.2 は、名前が sec- か proxy- で始まるヘッダと、Cookie・Origin などを forbidden request-header とし、スクリプトから設定させません。
These are forbidden so the user agent remains in full control over them.
Sec-Fetch-Site はこの接頭辞を持つので、値を決めるのはブラウザだけです。OWASP はこれを the primary signal for CSRF protection と呼びます。
- ・どちらの信号も「ブラウザの中で動く別サイトのページ」を相手にしている
同じ Cookie、違う判定
手元に b.test と a.test の2つのサイトを立てました(127.0.0.1、ローカル証明書)。b.test の GET /form はセッション Cookie sid と、セッションに結びつけた16バイトの乱数の CSRF トークンを発行し、トークンを hidden 欄に埋めたフォームを返します。POST /update は送られた値とセッションの値を crypto.timingSafeEqual で比べ、一致すれば accepted、それ以外は rejected を返します。Cookie はわざと SameSite=None にして、SameSite では止まらないようにしています。
要求 cookie csrf sec-fetch-site 判定b.test のフォーム -> /update sid=32d29495 あり same-origin ACCEPTa.test のフォーム -> /update sid=32d29495 なし cross-site REJECT
2行の cookie 列は同じ値です。区別したのは csrf 欄の有無で、ブラウザが付けた sec-fetch-site も same-origin と cross-site に分かれています。Playwright 同梱版(市販版ではない)の Chromium 153・Firefox 155・WebKit 26.6 で各2回、判定は同じでした。
a.test のスクリプトが、b.test への fetch に自分で `Sec-Fetch-Site: same-origin` を付けようとしました。実測では何が起きましたか。
「作れない」は、誰に対してか
2つの信号はどちらも、ブラウザの中で動く別サイトのページが作れない、という意味で作れません。相手を変えると成り立たなくなります。
- ブラウザ以外: curl はどんなヘッダでも付けられます。ただし被害者のブラウザの Cookie を持たないので、その要求は本人のものとして扱われません。
- 同じオリジンの中で動くスクリプト: 画面を読めるので CSRF トークンも読めます。OWASP は冒頭で
Remember that Cross-Site Scripting (XSS) can defeat all CSRF mitigation techniques!と書いています。
ヘッダの信号には、付かない場合もあります。W3C の Fetch Metadata は、要求先が potentially trustworthy な URL(https や localhost など)でなければ Sec-Fetch-* を付けません。Origin について Fetch Standard §3.2 はたとえば GET・HEAD 以外の要求に使うとしつつ Due to compatibility constraints it is not included in all fetches. と書き、referrer policy によっては値が null になります。OWASP も、Sec-Fetch-* を送らない旧ブラウザがあるとして a fallback to standard origin verification headers is a mandatory requirement としています。
Sec-Fetch-Site の値には same-site もあります。同じ site(scheme + eTLD+1)に属する別オリジン、たとえば兄弟サブドメインからの要求は cross-site になりません。「cross-site だけを拒否する」方針は、同じ site の中にいる相手を通します。
ページからは書けない、curl からは書ける
同じ Sec-Fetch-Site を、ブラウザの中のスクリプトと curl から付けようとしました。
送り手 付けようとした値 届いた sec-fetch-site 備考a.test のスクリプト(no-cors) same-origin cross-site 例外なしa.test のスクリプト(cors) same-origin cross-site OPTIONS 行なしcurl same-origin same-origin cookie なし、REJECT
cors モードでは、new Headers() にはキーとして入りました(headers-object-keys=[content-type,sec-fetch-site])。それでも要求には載らず、OPTIONS 行もありません(fetch は応答が CORS で許可されず TypeError)。Fetch Standard §5.1 は、guard が request の Headers に forbidden request-header を追加しようとすると検証が false を返し、手順はそこで return すると書いています。例外を投げずに黙って捨てるので、スクリプトはヘッダが捨てられたことに気づけません。
curl からは付けた値がそのまま届きました。Sec-Fetch-Site と Origin が意味を持つのは、ブラウザが付けた場合だけです。
「主要ブラウザが Sec-Fetch-Site を送るので、CSRF トークンや他の検証はもう要らない」という説明は妥当ですか。
CSRF トークン
サーバが乱数を発行して自分の画面に埋め、送られた値と照合します。別オリジンのページは画面を読めないので値を当てられません。同じオリジンの中で動くスクリプトは画面を読めるので、この防御は効きません。
相手 値を知る手段 防げるか別サイトのページ 無い 防げる同じオリジンで動くスクリプト 画面を読める 防げない
TCP の初期シーケンス番号(ISN)
RFC 9293 §3.4.1 は ISN を秘密鍵を入れた関数で作るよう求め(SHLD-1)、その関数は外から計算できてはならない(MUST-9)と定めます。想定する脅威は経路外の相手による当て推量です。経路上でパケットを見られる相手には番号がそのまま見えるので、効きません。
There are security issues that result ifan off-path attacker is able to predict or guess ISN values...ISN = M + F(localip, localport, remoteip, remoteport, secretkey)
片方は Web アプリ、片方は TCP。守っている範囲の境目は、どちらもどこに引かれているでしょうか。
「その値を見られる位置にいる相手」に対して、それぞれの仕組みが何をしてくれるかを考えてください。
この章のまとめ
- Cookie は本人の要求にも偽の要求にも付く。Cookie だけでは区別できない。
- 信号は2種類。別オリジンから読めない CSRF トークンと、スクリプトから書けない Sec-Fetch-Site・Origin。
- スクリプトが Sec-Fetch-Site を付けようとしても、ヘッダの追加では例外もプリフライトも起きずに捨てられる。curl なら任意の値を付けられる。
- どちらも「ブラウザの中の別サイトのページ」にだけ効く。同じオリジンの中のスクリプトは CSRF トークンを読める。
- ヘッダは付かない場合がある。OWASP はヘッダで判定する場合もフォールバックを mandatory とする。「トークンは要らなくなった」ではない。
- 部分一致とは別物。書けないヘッダを完全一致で見て、欠落時の扱いを決める。
自分の現場で確かめる
読んだだけでは身につきません。手元の環境で実際に見てみてください。
理解度チェック
読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。
OWASP CSRF Prevention Cheat Sheet が、同期トークンパターンの CSRF トークンに求めている性質として挙げていないものはどれですか。
用語集(3語)
- CSRF トークン(同期トークンパターン)
- サーバがセッションに結びつけて発行し、正規の画面に埋めて送らせる予測できない値。別オリジンのページはその画面を読めないので、値を用意できない。
- Sec-Fetch-Site
- 要求を始めた側のオリジンと宛先のオリジンの関係を示すヘッダ。値は cross-site・same-origin・same-site・none。potentially trustworthy な URL への要求にだけ付く。
- forbidden request-header
- Fetch Standard §2.2.2 が定める、スクリプトから設定できない要求ヘッダ。Cookie・Origin などの名前と、sec- / proxy- で始まる名前が含まれる。要求の Headers に追加しようとすると例外なしに捨てられる。
出典(一次ソース)
- OWASP Cross-Site Request Forgery Prevention Cheat Sheet(GitHub 版、コミット be333201dc8b・2026-09-11) ↗
- Fetch Standard(WHATWG、Last Updated 21 September 2026)§2.2.2 forbidden request-header / §3.2 Origin header / §5.1 Headers class ↗
- W3C Fetch Metadata Request Headers(Working Draft, 21 September 2026)§2.3 / §3 ↗
- RFC 9293 Transmission Control Protocol (TCP) §3.4.1 Initial Sequence Number Selection ↗