RailsのCSRF保護を詳しく調べてみた(翻訳)

概要 原著者の許諾を得て翻訳・公開いたします。 英語記事: A Deep Dive into CSRF Protection in Rails 公開日: 2017/07/31 著者: Alex Taylor サイト: Ruby Inside RailsのCSRF保護を詳しく調べてみた(翻訳) 現在Railsを使っていればCSRF保護を使うことがあるでしょう。この機能はRailsのほぼ初期から存在し、即座に導入して開発を楽にできるRailsの機能のひとつです。 CSRF(Cross-Site Request Forgery)を簡単に説明すると、悪意のあるユーザーがサーバーへのリクエストを捏造して正当なものに見せかけ、認証済みユーザーを装うという攻撃手法です。Railsでは、一意のトークンを生成して送信のたびに真正性を確認することでこの種の攻撃から保護します。 最近私がUnbounceのある機能を使ったとき、CSRF保護と、CSRF保護をクライアント側のJavaScriptリクエストでどう扱うかについて考慮が必要になりました。そのとき、自分がCSRF保護についてほとんど何も知らないどころか、CSRFが何の略語なのかも知らないことに気づきました。 そこで私は、Railsコードベースでこの機能がどのように実装されているかを詳しく調べることにしました。本記事では、RailsでのCSRF保護の動作を追ってみました。レスポンスごとのトークンが最初にどうやって生成されるか、およびサーバーへのリクエストの真正性のバリデーションについても解説いたします。 基本 CSRFには2つの要素で構成されます。最初にサイトのHTMLに一意のトークンを埋め込みます。これと同じトークンはセッションcookieにも保存されます。ユーザーがPOSTリクエストを送信するときに、HTMLに埋められていたCSRFトークンも一緒に送信されます。Railsはページのトークンとセッションcookie内のトークンを比較し、両者が一致することを確認します。 CSRF保護の利用法 Rails開発者はCSRF保護を無料で利用できます。最初に、application_controller.rbファイルでCSRF保護をオンにする以下の1行を有効にします。 protect_from_forgery with: :exception 次に、application.html.erbに次の1行を追加します。 <%= csrf_meta_tags %> これでおしまいです。この機能は長年Railsに搭載されているので、開発者はこれを利用するかどうか決めるだけでよいのです。しかしこの機能はどのように実装されているのでしょうか? 生成と暗号化 まずは#csrf_meta_tagsから調べてみましょう。これはHTMLに真正性トークンを埋め込むシンプルなビューヘルパーです(gist: csrf_helper.rb)。 # actionview/lib/action_view/helpers/csrf_helper.rb def csrf_meta_tags if protect_against_forgery? [ tag(“meta”, name: “csrf-param”, content: request_forgery_protection_token), tag(“meta”, name: “csrf-token”, content: form_authenticity_token) ].join(“\n”).html_safe end end csrf-tokenタグにご注目ください。すべてのマジックはここで起きます。tagヘルパーは#form_authenticity_tokenを呼んで実際のトークンを取り出します。そしてActionControllerのRequestForgeryProtectionモジュールに進むと面白くなってきます。 RequestForgeryProtectionモジュールは、CSRF関連の一切を取り扱います。中でも有名なのはApplicationControllerで見かける#protect_from_forgeryです。これはリクエストごとにCSRFバリデーションをトリガするフックを設定し、リクエストの真正性を照合できなかった場合のレスポンスを設定します。この他にもCSRFトークンの生成/暗号化/復号化も担当します。このモジュールはスコープが小さい点が気に入りました。ビューヘルパーを別にすれば、CSRF保護の実装は1ファイルに収まっています。 続いて、CSRFトークンがHTMLに達するまでを詳しく見てみましょう。 #form_authenticity_tokenは、セッション自身を含む任意のオプションパラメータを#masked_authenticity_tokenに渡すシンプルなラッパーメソッドです(gist: request_forgery_protection.rb)。読みやすさのためコードの一部を省略しています。 # actionpack/lib/action_controller/metal/request_forgery_protection.rb # トークンの値を現在のセッションに設定 def form_authenticity_token(form_options: {}) masked_authenticity_token(session, form_options: form_options) end # リクエストごとに異なる真正性トークンのマスキング版を作成する # マスキングはBREACHなどのSSL攻撃の緩和のため def masked_authenticity_token(session, form_options: {}) # :doc: # … raw_token = if per_form_csrf_tokens && action && method # … else real_csrf_token(session) end one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH) encrypted_csrf_token = xor_byte_strings(one_time_pad, raw_token) masked_token = one_time_pad + encrypted_csrf_token Base64.strict_encode64(masked_token) end Rails 5でフォームごとのCSRFトークンが導入されたため、masked_authenticity_tokenメソッドはやや複雑になっています。本記事では本来の実装である「リクエストごとに1つのCSRFトークンがmetaタグに達する」ところを追うことにします。この場合、上のelse分岐で#real_csrf_tokenの戻り値にraw_tokenが設定されます。 #real_csrf_tokenにsessionを渡す理由がおわかりでしょうか。このメソッドは実際には2つの動作を実行するからです。1つは暗号化されていない生トークンの生成、もう1つはトークンのセッションcookieへの埋め込みです(gist: equest_forgery_protection.rb)。 # actionpack/lib/action_controller/metal/request_forgery_protection.rb def real_csrf_token(session) # :doc: session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH) Base64.strict_decode64(session[:_csrf_token]) end このメソッドは最終的に、アプリのレイアウトの#csrf_meta_tagsが呼び出されると呼び出されることを思い出しましょう。これは昔ながらのRailsマジックです。この賢い副作用によって、セッションcookieのトークンがページのトークンと一致することが保証されます。保証される理由は、ページのトークンのレンダリングが行われるときには必ず同じトークンがcookieに挿入されるからです。 とにかく、#masked_authenticity_tokenの最後の方を見てみましょう(gist: request_forgery_protection.rb)。 # request_forgery_protection.rb one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH) encrypted_csrf_token = xor_byte_strings(one_time_pad, raw_token) masked_token = one_time_pad + encrypted_csrf_token Base64.strict_encode64(masked_token) ここでは暗号化が行われます。セッションcookieにはトークンを挿入済みなので、このメソッドはプレーンテキストHTMLで使われるトークンを返す作業にかかります。ここではいくつかの点に注意します(主にSSL BREACH攻撃の緩和のためですが、ここでは立ち入りません)。Rails 4以降はセッションcookie自体を暗号化するようになったため、セッションcookieに含めるトークンそのものは暗号化されない点にご注目ください。 最初に、生トークンの暗号化で使うワンタイムパッドを生成します。ワンタイムパッドは、長さの揃った平文メッセージをランダム生成キーで暗号化する手法で、メッセージの復号には同じキーが必要です。「ワンタイム(1回限り)」と呼ばれる理由は、メッセージごとに異なるキーを用い、利用後は破棄されるからです。Railsでは、CSRFトークンを新しく作成するたびに新しいワンタイムパットを生成し、平文トークンをビットごとのXOR操作で暗号化するためにこの機能を実装しています。このワンタイムパッド文字列は暗号化文字列の前に追加され、HTMLで使えるようBase64でエンコードされます。 CSRFトークン暗号化の仕組みの概要を図示します。デフォルトのトークンは長さは32文字ですが、ここでは12文字にしています。 操作が完了すると、マスクされた真正性トークンはスタックに戻され、アプリでレンダリングされたレイアウトに達します(gist: index.html)。 <!– index.html –> <meta name=”csrf-param” content=”authenticity_token” /> <meta name=”csrf-token” content=”vtaJFQ38doX0b7wQpp0G3H7aUk9HZQni3jHET4yS8nSJRt85Tr6oH7nroQc01dM+C/dlDwt5xPff5LwyZcggeg==” /> 復号と照合 CSRFトークンの生成と、トークンがHTMLとcookieに達するまでの解説が終わりましたので、次はRailsへのリクエストのバリデーションを見てみることにしましょう。 ユーザーがサイトにフォームを送信すると、フォームの他のデータとともにCSRFトークンが送信されます(デフォルトのparam名はauthenticity_tokenです)。トークンは、HTTPヘッダーX-CSRF-Tokenでも送信できます。 先ほどApplicationControllerに以下を追加したことを思い出しましょう。 protect_from_forgery with: :exception この#protect_from_forgeryメソッドは、すべてのコントローラアクションのライフサイクルの途中にbefore-actionを追加します。 before_action :verify_authenticity_token, options このbefore_actionで、リクエストのparamsやヘッダーにあるCSRFトークンと、セッションcookieとの比較を開始します(gist: request_forgery_protection.rb)。 … Continue reading RailsのCSRF保護を詳しく調べてみた(翻訳)