最近の CSRF 対策ってなにがいいんだろう
◆ Sec-Fetch での判断が良さそうだったけど Safari は未対応らしい
◆ Safari に対応させるならその他の方法
◆ 作るもの次第で楽なものを使う
◆ Safari に対応させるならその他の方法
◆ 作るもの次第で楽なものを使う
これまでやってた方法
これまで CSRF 対策にはトークンを使ってました私の場合は フォームでの POST は一切しなくて fetch での送信のみです
body に混ぜるよりは header に入れるほうが楽なのでヘッダーで送信します
トークンはユーザまたは Cookie に対して乱数で発行して リクエスト時に送信されたトークンがユーザや Cookie のトークンと一致することを確認しています
ヘッダーを見るならトークンは必要なかった
この方法って過剰だったようですfetch を使ったバックグラウンドでの通信の場合は ヘッダーで任意のデータを送信できますが フォームからではできません
標準でないヘッダーが送信されていることをチェックするだけでフォームからや直接アクセスは防げています
防ぐべきリクエストは外部のサイトからのリクエストです
自サイト内からのリクエストは防ぐ必要はありません
別オリジンからヘッダーを追加してリクエストする場合は GET/POST の前に OPTION メソッドの Preflight リクエストが発生します
このリクエストではリクエスト時に設定したヘッダーは送信されません
リクエストヘッダーをチェックしていれば この Preflight リクエストはブロックされて 本来のリクエストは届きません
サーバでは標準ではないヘッダーが送られてきていることだけチェックすれば中身は見なくても十分でした
全員が foo=bar というヘッダーを送ってそのチェックでもいいです
SameSite
トークンを考えなくてもいい時点で楽になりましたが まだクライアント側でリクエスト時にヘッダーを追加する必要があります共通して呼び出す api 関数などに書いておけば別に気にするほどでもないのですが 送らずに済ませられるならそうしたいです
他の方法では Cookie の SameSite を使う方法もあります
別のサイトへのリクエストなら Cookie を送信せず ブラウザへの保存もしなくできるものです
投稿するとかログアウトするとかアカウントを削除するとかのリクエストを勝手に送られても Cookie がなくてログイン状態とみなされなければ大抵の処理は実行できず無意味に終わります
ログインのリクエストのようなログイン状態が必要ない処理は行われますが ブラウザがレスポンスにある Set-Cookie ヘッダーを無視するのでそのブラウザではログインできていないことになります
ですがこれってログイン前提のシステムじゃなければ意味がないと思います
掲示板みたいなログイン不要なところへの書き込みであれば Cookie が送られてなくても書き込みができてしまいます
これを対策するなら前述のような方法が必要になります
Origin
最近だと Origin を見る方法を使ってるところも見かけますブラウザが自動で送るヘッダーで どのオリジンからリクエストされたかが含まれています
JavaScript での書き換えはできないので偽装はできません
自動で送られるのでクライアント側での追加処理は不要で サーバ側で送られてきた Origin ヘッダーが自分のオリジンに一致するかをチェックします
便利そうなのですが サーバが自分のオリジンを知る必要があります
サーバが自身のオリジンを知らないことって普通にあると思います
それにオリジンが 1 つとは限らないのでユーザに合わせるために オリジン付きで URL を生成するときにはユーザが送ってきた情報をもとに作ったりします
リバースプロキシを経由するならユーザがアクセスするオリジンはリバースプロキシのものです
複数のリバースプロキシがあってユーザによってアクセスしているオリジンが変わる可能性もあります
サーバ側で管理してるなら設定で対処できますが ユーザ側でのプロキシやポートフォワードや hosts ファイル設定によって サーバ側で意図したオリジンとは異なるオリジンでアクセスしてる可能性もありえます
あと POST でしか送信されません
GET でサーバ側のステータスを変更するようなことをは避けるべきだとは言っても そういうケースが無いとはいえません
必要なったときに全体のやり方自体を変えないといけなくなるのは採用しづらい気持ちもあります
Fetch Metadata Headers
Sec-Fetch
いつの頃からかブラウザからのリクエストに Sec-Fetch-* 系のヘッダーがついていますSec-* 系は Origin みたいにブラウザが自動で送るもので JavaScript で変更できません
Forbidden header name という名前がついてます
Sec-Fetch-* 以外に UA 代わりの Client Hints が Sec-CH-UA で送られてたりします
Sec-Fetch-* には
Sec-Fetch-Dest
Sec-Fetch-Mode
Sec-Fetch-Site
Sec-Fetch-User
があって リクエストに関する情報が入っています
Dest ではどういうコンテキストでのリクエストかがわかります
document や image や script などです
Mode には navitate や no-cors や cors などが入ります
img や script タグでのリクエストなら no-cors ですが type="module" の script や fetch でのリクエストなど CORS の制限を受けるリクエストは cors になります
Site はリクエスト元がどういう関係のところかを表します
値の種類は cross-site, same-origin, same-site, none で見たままです
ユーザが URL を直接入力したりブラウザ外から開いたりすると none になります
User はユーザの操作でリクエストされたものかを表す boolean 型です
boolean 型は HTTP ヘッダーの場合は ?0 か ?1 で表すようです
ただの 0/1 だと数値型と区別できないからでしょうか
このヘッダーの場合は ?0 になるならヘッダーが送信されないみたいです
CSRF 対策に
Sec-Fetch-Site を使えば Origin の問題点を解決して 別サイトからのリクエストをブロックできますsame-origin と same-site で分かれているので 完全に同じオリジンからのみしか受け付けないことも サブドメインやスキームやポートが違うけど同じサイトなら許可するということもできます
自分でヘッダーを追加する方法だと 同じオリジンからでもフォームを使った POST はヘッダーを追加できないのでブロックされます
この方法だとフォームからでも fetch でも送られるので正規のリクエストはどちらの方法も使えます
fetch のみに限定したいなら Sec-Fetch-Dest が empty かどうかをチェックします
Safari
特に欠点もなく Sec-Fetch ヘッダーを使う方法で良さそうですいまだともう IE なんて考える必要もないですし
なんて思っていたら Safari が対応してませんでした
MDN の Browser compatibility を見ると✕になってます
相変わらず足手まといなブラウザですね……
Chrome/Edge のみのページなら気にせず使っても Safari だと必要なヘッダーが送られず不正扱いになるだけのはずです
Sec-* が Forbidden header name として認識されているならですけど
もしこれもされてないなら 悪意あるページから JavaScript で same-origin と偽ってリクエストを送ってこれます
こういうときに Windows だと動作確認して見れないのが面倒なところです
Playwright には Webkit もありますが こういうところも完全に Safari 準拠なんでしょうか
とりあえず Safari 対応が必要なら Sec-Fetch は使えません
if Safari はサポートしない
⇢ Sec-Fetch ヘッダーを見る
else if Origin の不便なところが許容できる
⇢ Origin ヘッダーを見る
else if ログインが前提
⇢ Cookie の SameSite
else if fetch のみでフォームからの POST はしない
⇢ 固定のヘッダー追加
else
⇢ フォームでトークン送信
という感じでしょうか