クッキーだけではリクエストの出どころは分からない
一言でいうと
CSRFは、ブラウザーがCookieを自動で付ける性質を借りて、別のサイトにユーザーに黙って状態を変えるリクエストを送らせる攻撃です。Cookieだけを見ても、そのリクエストが自分たちのページから始まったのかはわかりません。そのためサーバーは、攻撃ページが真似できないシグナルを別に要求する必要があります。
なぜ必要なのか
ユーザーがbank.example.comにログインしたまま、別のタブで攻撃者のページを開きます。そのページにはbank.example.com/transferへ向かうフォームが隠れていて、開いた瞬間に送信されます。ブラウザーは宛先が銀行なので、銀行のCookieを付けます。サーバーに届いたリクエストは、ユーザーが自分で押した送金と1文字も違いません。
攻撃者はレスポンスを読めません。クロスオリジンのレスポンスは同一オリジンポリシーが隠すからです。そのためCSRFは盗む攻撃ではなく書き込む攻撃で、防御も「この書き込みは本当に自分たちのページから来たのか」を確認する方向へ発展しました。
どう動くのか
OWASP CSRF防止チートシートがまとめている防御は4系統です。
同期トークン(synchronizer token)。サーバーがセッションごとに予測できない値を作ってセッションに保存し、ページに返します。状態を変えるリクエストは、その値をヘッダーや隠しフィールドで送り返す必要があります。攻撃ページは同一オリジンポリシーのためその値を読めません。OWASPは、トークンをCookieで送らず、フォームフィールドよりカスタムヘッダーを使うよう勧めています。ここでよく抜けるのがセッションへの紐付けです。「自分たちが発行したトークンか」だけを見ると、攻撃者が自分のアカウントで受け取った本物のトークンを被害者のリクエストに入れても通ってしまいます。比較は、時間が漏れないようにhmac.compare_digestのような定数時間の関数で行います。
署名付きダブルサブミットCookie。サーバーに状態を持ちたくないときに使います。ランダムな値とセッションIDを結び付けてHMACで署名した値を、Cookieとヘッダーの両方に載せます。署名なしでCookieとヘッダーが同じかどうかだけを見る方式は、OWASPは勧めていません。サブドメインやネットワーク上の位置からCookieを注入できる攻撃者は、2つの値を同じに揃えられるからです。
Fetch MetadataとOrigin。ブラウザーはリクエストごとにSec-Fetch-Siteを付けます。値はsame-origin・same-site・cross-site・noneで、Sec-で始まる禁止ヘッダーなのでページのスクリプトは変更できません。OWASPは、cross-siteのPOST・PUT・PATCH・DELETEを拒否し、same-siteは兄弟サブドメインを信頼するときだけ許可するよう述べています。ヘッダーのない古いクライアントは、ブロックするか(機微なエンドポイントで推奨)、Originの検査とトークンに回して判断します。Originヘッダーは、許可リストと文字列全体が同じかどうかを見ます。前半だけを比較すると、http://127.0.0.1:8303.evil.exampleのような名前が通ってしまいます。
カスタムヘッダー。クロスオリジンのリクエストにカスタムヘッダーを付けるには、CORSのプリフライトリクエストを通過する必要があります。サーバーが許可しなければ、リクエストは送られません。そのためCORSをゆるく開けると、この防御も一緒に崩れます。
SameSiteは補助です。RFC 6265bis草案では、LaxのCookieは、同じサイトのリクエストと、クロスサイトでも安全なメソッドのトップレベル移動には載ります。限界が3つあります。
same-site ≠ same-origin blog.example.com 과 bank.example.com 은 같은 사이트다
GET 으로 상태 변경 링크 하나로 최상위 GET 이동이 되고 Lax 쿠키가 실린다
기본값의 예외 속성이 없으면 Lax 와 같은 기본 모드지만 브라우저가
Lax-allowing-unsafe 를 쓸 수 있다
最後の行について、草案はそのようなブラウザーに対して、最近作られたCookieにだけ例外を設けるよう求めており、2分以下を妥当な上限として挙げています。web.devによると、Chromeは80から、属性のないCookieをLaxとして扱いつつ、トップレベルのPOSTに約2分間Cookieを許可する一時的な緩和を設けました。明示的なSameSite=Laxには、この例外はありません。OWASPの結論も同じです。SameSiteは多層防御にすぎず、CSRF対策の代わりにはなりません。
ログインフォームも対象です。ログインCSRFは、被害者を攻撃者のアカウントでログインさせ、被害者が入力したものが攻撃者のアカウントに溜まるようにします。ログイン前はセッションがなくトークンを紐付ける先がないので、OWASPは、ログイン前のセッションを作ってトークンを載せ、ログイン後にはセッションを作り直すよう述べています。
現場での姿
GET /logoutやGET /approve?id=42のように、GETで状態を変えるエンドポイントが残っていると、SameSite=Laxは何も防げません。リンク1つでCookieが載るからです。OWASPが「状態を変える操作にGETを使わない」と別に書いている理由です。
会社のサービスが1つの登録可能ドメインの下で複数のサブドメインに分かれているケースもよくあります。キャンペーンページ1つにXSSができると、そのページは銀行のサブドメインにsame-siteのリクエストを送ります。SameSiteは通過してしまい、兄弟を信頼しないFetch Metadataのポリシーと、セッションに紐付いたトークンだけが防いでくれます。
次のラボですること
127.0.0.1:8303にログインできる送金サーバーを立て、セッションごとのCSRFトークンを発行します。採点ツールがcurlで偽造リクエストを直接作ります。トークンなしのリクエスト、他人のセッションのトークン、Sec-Fetch-Site: cross-site、許可リスト外のOriginを送り、すべて403になり、送金が記録されていないかまで確認します。最後に、SameSite=LaxのCookieがどのクロスサイトリクエストに載るかの判定表を埋めます。