InvalidAuthenticityToken探検隊
はじめに
本記事ではRailsのCSRF対策についてソースコードを追って深掘りしていますが、あくまでコードを読んだ個人の解釈であり、セキュリティ対策の最終的な判断と実装は読者自身の責任となることご理解ください。もし誤った内容を言及している箇所などありましたら優しくコメントをしていただけると幸いです。
目的
先日同じチームのメンバーがInvalidAuthenticityToken関連のタスクを対応していました。自分も過去に対応したことあるはず?ですが正直そんなにこの例外を理解しているわけではなく「あぁCSRFのやつね」という感じの理解なので、本記事では改めて①CSRFがどのような攻撃で②Railsがそれに対してどのように対策を講じていて、③どのようなパターンでInvalidAuthenticityTokenを発生させているのかをソースコードを追って深堀りしていきます。
CSRFとは
CSRFとはCross Site Request Forgeryの略でForgeryとは「偽造、贋造」という意味で、言葉の意味するところだけを汲み取ると異なるホストのWebサイトへ偽造されたリクエストを送る攻撃であると言えそうです。
具体的な内容を確認するにはIPAやCloudflareなどのドキュメントが参考になります。
ターゲットのサイトのセッションを悪用し意図しないデータの操作を行わせるというのがこの攻撃の特徴です。直接的にデータを抜き取るということはなさそうですが、管理者の認証情報を変更すれば重大な被害が出る可能性があります。
実際にサンプルサイトもありCSRFを体験することもできるのでご興味のある方はぜひ。
RailsにおけるCSRF
ではRuby on RailsではどのようにしてCSRFを防いでいるのでしょうか。
Guideに記載がありますがデフォルトでセキュリティトークンが導入されておりこれによりCSRFを防いでいるようです。
設定を有効にしアプリケーションコントローラに以下のコードを追加することでRailsで生成する全てのフォームとAjaxリクエストにトークンが付与され、それをサーバー側で検証することでCSRFを防いでいます。protect_from_forgery with: :exception
このセキュリティトークンはサーバー側でどのように検証され、そして上記で述べたInvalidAuthenticityTokenとどのような関係があるのでしょうか。
以下ではInvalidAuthenticityTokenの発生条件とセキュリティトークンの検証の仕組みとをソースコードを追って深掘りしてみます。
InvalidAuthenticityTokenエラーとは
CSRF対策は以下のファイルで行われているようです。
上記コメントにあるようにActionController::Base のサブクラスは、デフォルトで :exception
戦略で保護されており、検証されていないリクエストに対して ActionController::InvalidAuthenticityToken エラーを発生させているようです。※戦略は protect_from_forgery
の with:
オプションの値の部分になります。
ではどのようにリクエストを検証しているのでしょうか。
InvalidAuthenticityTokenエラーが発生するパターン
例外発生箇所
ActionController::InvalidAuthenticityTokenは以下で raise
されています。
この handle_unverified_request
は verify_authenticity_token
で呼び出されており、戦略が :exception
且つ verified_request?
が false の場合に例外が発生するようになっています。
文字通りリクエストの検証は verified_request?
で行っているようですが、メソッドの中身を見ると実態は valid_request_origin? && any_authenticity_token_valid?
のようです。
valid_request_origin?
こちらはリクエストが同じオリジンからのものかどうかを確認しています。デフォルトは true を返し、リクエストのオリジンが文字列で null
の場合、若しくは base_url と同一の場合は true を、それ以外は false を返します。
any_authenticity_token_valid?
恐らくリクエスト検証の本丸はここです。
リクエストに含まれるトークン( request_authenticity_tokens
)を valid_authenticity_token?
で検証して一つでも true を返すとメソッド自体が true を返すようです。
request_authenticity_tokens
こちらはリクエストに含まれるトークンを配列に格納しています。要素はパラメーターに含まれるトークンとヘッダーに含まれるトークンの2つになります。
inputタグの authenticity_token
の値がパラメータートークンとして、
リクエストヘッダーの X-CSRF-TOKEN
の値がヘッダートークンとして格納されます。
valid_authenticity_token?(session, token)
こちらはリクエストに含まれるトークンを検証するメソッドになります。コメントにある通り、クライアントのマスクされたトークンがセッショントークンと一致するかを確認します。
引数で渡した token を Base64 でデコードしたトークンと、セッションが保持するトークンとを安全に比較しているようです。* ActiveSupport::SecurityUtils.fixed_length_secure_compare
はここでは言及しないです。機密情報の文字列を安全に比較し同一だと true を返すメソッドだと思ってください。
ここでいう real_csrf_token(session)
がセッションが保持するトークンになります。
real_csrf_token(_session = nil)
こちらがセッション側のトークンを取得するメソッドになります。ちょっと難解💦
Hash#fetch は与えられたキーが要素にない場合にブロックの内部を評価します。(参照)
この場合、 request.env
(=リクエストを受け付けて処理する際のRackの環境設定)というハッシュオブジェクトにCSRF_TOKENという環境変数で設定した文字列のキーがあればその値を返し、
もし無ければセッション情報からCSRFトークンを取得するようになってます。セッション情報にトークンが存在しない場合新たにトークンを生成して返しているのですがこれは何故だろう 🤔 (real_csrf_tokenと言えるのだろうか。。。)
取得したトークンはデコードしてクライアント側のトークンと比較する処理に渡されます。
おわりに
ちょっと雑にソースコードを追ってみましたが、Railsセキュリティガイドにある通りクライアント側のトークンをサーバー側で検証する流れを確認することができました。また、InvalidAuthenticityToken
は verified_request? メソッドの判定に関わりがあり、フォームに付与されたトークン若しくはリクエストヘッダーに含まれるトークンと、Rackの環境設定やセッションが保持するトークンを比較し一致しない場合に例外が発生するようになっていました。(関係ないですがこのファイルのメソッドの命名が素晴らしくてコード追いやすかったです。)
ここに記載のあるのは :exception
戦略の際に呼び出される処理のほんの一部ですが、他の戦略やクライアント側へのトークンの生成・付与の処理も追って理解を深められればと思いました。
Discussion