こんにちは @zaru です。今回は昔からある CSRF (クロスサイト・リクエスト・フォージェリ) の今時の対策についてまとめてみました。もし、記事中に間違いがあれば @zaru まで DM もしくはメンションをください (セキュリティの細かい部分についての理解が乏しい…) 。
- 2022/08/29 : 徳丸さんからフィードバック頂いた内容を反映しました。徳丸さん、ありがとうございます!
- 認証あり・なしで対策方法が違う点
- トークン確認方式のデメリットのクロスドメインについての言及を削除、代わりに Cookie 改変リスクを追記
- Cookie 改ざん可能性について徳丸さんの動画リンクを追記
- SameSite 属性で防げない具体的なケースを追記
- nginx 説明が関係なかったので削除
そもそも CSRF ってなに?
昔からインターネットをやっている方であれば「ぼくはまちちゃん」 騒動と言えば分かるかもしれません。2005年頃に mixi で「ぼくはまちちゃん! こんにちはこんにちは!!」という定型の投稿が、投稿者が意図せず書き込まれてしまったという事件です。CSRF はまさにこの「投稿者が意図せず」というところがポイントです。
CSRF は、攻撃者が仕掛けたページに被害者 A さんを誘導するところから始まります。A さんは一見なにもないページを閲覧しただけですが、裏では JavaScript を使って別サービス (図で言う BBS) に書き込みリクエストを送信するようになっています。結果、BBS のサーバから見ると、A さんの閲覧環境からのリクエストとして処理されてしまいます。
ざっくり言うと、これが CSRF です。攻撃方法はさまざまありますし、脆弱性さえあれば、会員としてログイン済みのサービスで勝手に退会処理を強制されたりなど様々な影響が出てしまいます。怖いですね。
CSRF 対策ってどうすれば良いの?
結論から言うと以下の2点です。
- 現代的なフレームワーク・ライブラリを使う
- 上記ツールの CSRF 対策機能をオフにしない
CSRF というのは昔からある攻撃で現代的なフレームワークやフォームライブラリなどであれば標準で対策がされています。つまりアプリケーションの開発者は自前で対策をしなくても、ツールのルールに従ってコードを書いていれば必要十分な対策はできていることになります (当然、100% これで防げるという保証をするものではないですが)。
自ら脆弱性を作ってしまう行為
CSRF 対策を機能としては持っていても何らかの事情で機能をオフにしてしまうと意味がありません。例えば Rails で「なぜか ActionController::InvalidAuthenticityToken
例外が出て動かない… skip_forgery_protection
を指定したら動いたぞ」みたいな記事をよく見ます。
しかし理解せず skip_forgery_protection
を指定すると自ら脆弱性を作ってしまう危険な行為です。一般的な Web アプリケーションであれば CSRF 対策をオフにする必要はあまりないので、そういった時には一度立ち止まって他に間違っているところがないかを考えるようにした方が良いでしょう。
また、機能をオフにはしていないが CSRF 対策から外れるコードを書くこともあり得ます。例えば GET リクエストで 状態を変更するような API を作ってしまう等です。Web アプリケーションの作法として状態変更をする API は POST メソッドにするというものがあり、フレームワークの CSRF 対策も POST 時にしか有効にならなかったりします。そのため、使っているフレームワークやライブラリの CSRF 対策機能を調べて保護されるケースを理解することも重要です。
他にもフレームワークやブラウザ自体の脆弱性、あるいは CSRF 以外に XSS などの脆弱性があると CSRF 対策をしていたとしても抜け道ができてしまうので全体のセキュリティ対策は必要になります。
代表的な CSRF 対策手法
代表的な CSRF 対策の手法をいくつか紹介します。
- トークンの確認
- カスタムリクエストヘッダの確認
- SameSite Cookie Lax/Strict 設定
- Double Submit Cookie
- Origin リクエストヘッダの確認
- Sec-Fetch リクエストヘッダの確認
フレームワークでは単独の手法だけではなく、複数組み合わせていることもあります。Rails ではトークンの確認と Origin ヘッダの確認を併用しています。以下ではそれぞれの特徴を簡単に解説します。もし間違っていたら教えてください。
また、認証を必要とするケースと必要としないケースで CSRF 対策も異なる点にも注意です。認証を必要とするというのは例えば SNS や会員ページなどのログインを必要とするケースです。認証には Cookie でセッション管理をする前提で話を進めます。
トークンの確認
昔からある、おそらくもっとも使われている対策です。細かい実装方法はフレームワークやライブラリによって異なりますが、雑に言うと一時的なランダムトークンをサーバで発行し、セッションとして保持。ブラウザにはセッション Cookie と、トークンを <input type="hidden" value="token-xxx">
とフォームパーツとして含んだ HTML を返します。
POST された際に、フォームの送信データのトークンと、セッションで保持しているトークンが一致するかで正規なリクエストかどうかを判断します。
メリットは手堅いところ、デメリットは自前で実装する際のコストが高いところです。
また、認証を必要としないケースでは、後述する Double Submit Cookie と同様に Cookie を改変されてしまうとトークンチェックをパスできてしまう可能性があります。
カスタムリクエストヘッダの確認
POST リクエストする際に、独自のカスタムヘッダを指定し、サーバ側で確認する手法です。Ajax など fetch()
メソッドを使ったリクエストをする際に採用されたりします。fetch()
でカスタムヘッダを指定すると、プリフライトリクストという OPTIONS メソッドのリクエストが自動で発行されます。
サーバ側は OPTIONS リクエストの中身を見て許可するかどうかを判断します。また許可された後に本来の POST リクエストが飛び、そのリクエストヘッダの中身をみてカスタムヘッダが正規なものかどうかで追加の判断をすることができます。
メリットはシンプルな実装になること、デメリットは <form>
をつかった POST は保護できないことです。
SameSite Cookie Lax/Strict 設定
2020 年リリースされた Chrome 84 から Cookie の SameSite 属性は Lax がデフォルト値になりました。SameSite 属性というのは Cookie 制御をファーストパーティのみにするかどうかを指定することできます。3つの値があります。
- Lax : 現代ブラウザのデフォルト値。異なる Origin からのリクエストにおいて、トップレベルナビゲーションかつ GET リクエスト時に Cookie 送信を許可します。通常のリンク遷移であれば Cookie 送信されますが、
iframe
では GET でも送信されません - Strict : ファーストパーティのみ送信、異なる Origin からのあらゆるリクエストにおいて送信されません
- None : かつてのデフォルト値です。どの状態でも Cookie を送信します。ただし
Secure
属性も合わせて指定をしないと機能しません
この手法は CSRF 攻撃をある程度緩和させるためのものなので、単独で十分な CSRF 対策にはなり得ません。十分でないケースとしてはいかが考えられます (徳丸さんからフィードバックいただきました) 。
- GET リクエストで更新処理を許してしまう
- 認証を必要としないケース
- SameSite 属性に対応していない古いブラウザを使っているケース
カジュアルに防げるケースだと、Lax / Strict であれば、ログイン (Cookie) が必要なアプリケーションの API を、外部から勝手にリクエストを強制させる攻撃は防ぐことができます。Cookie が送信されずログアウト状態のリクエストになるため。
Double Submit Cookie
仕組みとしては最初に紹介したトークンの確認とほぼ同じです。違いは前者はトークンをセッションデータに保持しています (例えば Redis などに) 。つまりサーバ側がトークンを保持しています。しかしこの Dobule Submit Cookie は、トークンを Cookie として渡します。その後、POST 時に Cookie のトークンと、リクエストボディにトークンを埋め込んでサーバへ送信します。サーバは送信された二つのトークンが一致するかを確認します。
サーバは発行したトークンを保持しない代わりにユーザに Cookie で保持してもらうという点が特徴です。これは Cookie が改変されないという前提に立った手法になります。Cookie が改変されにくいようにするためにはいくつか方法があります。
- HTTPS で Secure 属性を使用
- HTTP Strict Transport Security を使用
- Cookie 名に
__Host-
プレフィックを使用
__Host-
プレフィックス
__Host- で始まるクッキー名は、 secure フラグを設定し、安全なページ (HTTPS) から読み込む必要があり、ドメインを指定することができず (従って、サブドメインにも送られません)、パスが / である必要があります。
また、Cookie の改ざんについては徳丸さんの YouTube 動画で詳しく解説されていますので、是非参照をして見てください。
Origin リクエストヘッダの確認
仕組みとしては今まで紹介した手法の中でも最もシンプルです。ブラウザが付与する Origin リクエストヘッダ と、サーバが動いているホストを比較して一致する場合に正規なリクエストとして処理し、不一致の場合は想定していないサイトからのリクエストとして拒否します。
メリットは実装が非常にシンプルで簡単なこと。デメリットはサーバが自身のホスト名を確定できることが条件であること。とはいえ一般的な Web アプリケーションであればホスト名は確定できるので問題にはならないと思います。
繰り返しになりますが、GET リクエストで状態を変更するような API がないことが前提です。Origin リクエストヘッダは CORS や POST リクエスト時にしか付与しません。つまり GET では Origin リクエストヘッダの確認はできない点に注意してください。
Sec-Fetch リクエストヘッダの確認
これは前述の Origin リクエストヘッダと同じ仕組みでありつつ、さらに楽な手法になります。Sec-Fetch リクエストヘッダとはブラウザが自動で付与してくれる情報です。以下の4つがあります。
- Sec-Fetch-Dest : リクエスト元が fetch するりソースを使う形態 e.g. document / iframe
- Sec-Fetch-Mode : リクエストの形態を表す e.g. cors / navigate
- Sec-Fetch-Site : リクエスト元オリジンとリクエストされたオリジンの関係 e.g. same-origin / cross-site
- Sec-Fetch-User : ユーザ自身がリクエストを発行したかどうか、その場合において値は必ず
?1
この中でも CSRF 対策としては Sec-Fetch-Site
リクエストヘッダを使います。Sec-Fetch-Site
が same-origin
の場合は同一オリジンであることが保障されます。つまり前述の Origin リクエストヘッダでは実際にホスト名を取得して比較していたのが、事前にブラウザ自身が同一性を比較した結果だけを受け取ることができます。大変便利ですね。
しかし、この方法には唯一の欠点があります。それは 2022-08-27 時点で Safari がまたサポートしていない点です。
Safari がサポートをしてくれれば、Sec-Fetch-Site
リクエストヘッダを確認するのが最も手軽な CSRF 対策になりそうです。
まとめ
CSRF 対策はいくつかありますが、最初に書いたように現代的なフレームワークを使い、フレームワークの流儀にのったコードで実装することが一番の対策になるかなと思います。また、なにをすると流儀から外れ防御されなくなるのかを知るために、CSRF 対策手法そのものを理解することが大事だと改めて感じました。
セキュリティ対策は時代の流れとともに変わっていくので、これからも情報を追いつつ実験していきたいと思います。
最後に、もし記事中に間違いがあれば @zaru まで DM もしくはメンションをください。