私の歴史と今

振り返ると恥ずかしくなるのが私の歴史。だけどそのときは真面目に書いていた訳でね。そんな今の私を書いていく。

RailsのCSRF対策について

scaffoldで作成したアプリの新規登録画面(/new画面)に下記の隠しフィールドがあることに今更ながら気づいた・・・)

<div style="margin:0;padding:0;display:inline">
  <input name="authenticity_token" type="hidden" 
         value="5NinTTI93SY3+Oiw/+rUR+06MunmCYCDh1unc3KPMGk=" />
</div>

authenticityは、「信憑性」という意味らしい。ということは、Railsはこのトークンを使って何かしらの信憑性を判断しているようだ。
そこでググってみたところ、CSRFの対応について、rails使いが知っておくべきことというブログが引っ掛かった。内容を読むと、CSRF対策として、先のauthenticity_tokenを使用しているとのこと。以下、引用です。

railsは、get以外の動詞のリンクに、authenticity_tokenというパラメータを自動的に付け加えます。get以外の動詞の各アクションでparams[:authenticity_token]とsession[:csrf_id]を比較して、同値であればOKとしているようです。同値でなければActionController::InvalidAuthenticityTokenという例外がでます。

CSRFの対応について、rails使いが知っておくべきこと - おもしろWEBサービス開発日記

なるほど。確かにsessionを出力してみると、下記のようにauthenticity_tokenと同値の項目がある。

{:session_id=>"992efe0bd6d1ceaaee64aa992e4c9fa2", 
 :hoge=>"hoge", 
 :aaa=>"aaa", 
 :_csrf_token=>"5NinTTI93SY3+Oiw/+rUR+06MunmCYCDh1unc3KPMGk="}

でも「csrf_id」じゃなく「_csrf_token」になっている。当然のことながら、Cookieの中にも存在している。ハッシュ値の前の部分をBase64.decode64してみると、以下のように「_csrf_token」が存在していることがわかる。

"\004\b{\n:\017session_id\"%992efe0bd6d1ceaaee64aa992e4c9fa2:\thoge\"\thoge:\baaa\"\baaa:\020_csrf_token\"15NinTTI93SY3+Oiw/+rUR+06MunmCYCDh1unc3KPMGk=\"\nflashIC:'ActionController::Flash::FlashHash{\000\006:\n@used{\0007"

GET以外のHTTPメソッドを使用した時に、リクエストパラメータ内のauthenticity_tokenとセッション内の_csrf_tokenの値を比較し、異なっていたらActionController::InvalidAuthenticityTokenという例外が発生するそうなので、リクエストパラメータを1文字変更してPOSTしてみた。するとやはり同様の例外が発生した。

ActionController::InvalidAuthenticityToken in SectionsController#create

ActionController::InvalidAuthenticityToken

RAILS_ROOT: C:/Users/ken/Documents/NetBeansProjects/ERP
Application Trace | Framework Trace | Full Trace

c:/jruby-1.5.0/lib/ruby/gems/1.8/gems/actionpack-2.3.8/lib/action_controller/request_forgery_protection.rb:79:in `verify_authenticity_token'
c:/jruby-1.5.0/lib/ruby/gems/1.8/gems/activesupport-2.3.8/lib/active_support/callbacks.rb:178:in `evaluate_method'
(略)
Request

Parameters:

{"authenticity_token"=>"5NinTTI93SY3+Oiw/+rUR+06MunmCYCDh1unc3KPMGK=",
"section"=>{"name"=>"システム"},
"commit"=>"Create"}

(略)

authenticity_tokenの最後の「k」を「K」に変更したことがわかる。
ちなみに、隠しフィールドの値を変更するためには、ie8であれば「F12」ボタンで起動する開発ツールを使えばできる。私はFirefoxFirebugで変更した。Cookieの編集も簡単にできる。

例外が発生したソースActionController::InvalidAuthenticityToken(77行目あたり)を見てみる。

      # The actual before_filter that is used.  Modify this to change how you handle unverified requests.
      def verify_authenticity_token
        verified_request? || raise(ActionController::InvalidAuthenticityToken)
      end
      
      # Returns true or false if a request is verified.  Checks:
      #
      # * is the format restricted?  By default, only HTML requests are checked.
      # * is it a GET request?  Gets should be safe and idempotent
      # * Does the form_authenticity_token match the given token value from the params?
      def verified_request?
        !protect_against_forgery?     ||
          request.method == :get      ||
          request.xhr?                ||
          !verifiable_request_format? ||
          form_authenticity_token == form_authenticity_param
      end

      def form_authenticity_param
        params[request_forgery_protection_token]
      end

      def verifiable_request_format?
        !request.content_type.nil? && request.content_type.verify_request?
      end
    
      # Sets the token value for the current session.  Pass a <tt>:secret</tt> option
      # in +protect_from_forgery+ to add a custom salt to the hash.
      def form_authenticity_token
        session[:_csrf_token] ||= ActiveSupport::SecureRandom.base64(32)
      end

verify_authenticity_tokenメソッドで、verified_request?がfalseになると例外が発生する。ソースを追っていくと、verified_request?メソッドがfalseを返すのは以下条件が揃った時だとわかる。

    1. protect_against_forgery?がtrueの時(テスト時はtest.rbで「config.action_controller.allow_forgery_protection = false」となっているが、それ以外はtrue)
    2. GETメソッド以外の時
    3. XHR(XMLHttpRequest)リクエスト以外の時
    4. content_typeがnilじゃなく、content_typeが(:html, :url_encoded_form, :multipart_form, :text)のいずれかの時(つまり、html/textや、ファイルアップロード時)。
    5. form_authenticity_token(セッション内の_csrf_token) != form_authenticity_param(リクエストパラメータのauthenticity_token)の時。

結論

Railsは、GETメソッド以外のHTTPリクエストが来た場合、リクエストパラメータ内のauthenticity_tokenとセッション内の_csrf_tokenの値を比較し、

    • 同値であれば同一アプリ内からのリクエストであると判断して処理を進める。
    • それに対して、値が異なっている場合は、他サイトからのリクエストだと判断し、正常リクエストとして受け付けない。具体的には、「ActionController::InvalidAuthenticityToken」という例外を発生させ、業務ロジックを実行させないようにしている。

なお、検証した環境は以下のとおりです。