LoginSignup
14
15

More than 5 years have passed since last update.

RailsのInvalidAuthenticityToken対応

Last updated at Posted at 2017-11-20

システム

  1. /ページで部屋一覧を表示する。
  2. 部屋一覧ペーシから/entry/:id に移動して入室用ページを開く
  3. 入室パスワードをFormから入力すると/entry/にPOSTする
  4. POSTを受け取ると無条件に"Not Implemented"を表示する。

現象

POST時にSessionsController#createInvalidAuthenticityToken例外が発生する。

ログ

Started GET "/" for 127.0.0.1 at 2017-11-20 22:16:55 +0900
Processing by RoomsController#index as HTML
  Rendering rooms/index.html.erb within layouts/application
  Room Load (0.3ms)  SELECT "rooms".* FROM "rooms"
  Rendered rooms/index.html.erb within layouts/application (2.3ms)
Completed 200 OK in 24ms (Views: 21.7ms | ActiveRecord: 0.3ms)


Started GET "/entry/1" for 127.0.0.1 at 2017-11-20 22:16:59 +0900
Processing by SessionsController#new as HTML
  Parameters: {"id"=>"1"}
  Room Load (0.1ms)  SELECT  "rooms".* FROM "rooms" WHERE "rooms"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  Rendering sessions/new.html.erb within layouts/application
  Rendered sessions/new.html.erb within layouts/application (4.4ms)
Completed 200 OK in 33ms (Views: 30.4ms | ActiveRecord: 0.1ms)

Started POST "/entry/" for 127.0.0.1 at 2017-11-20 22:17:19 +0900
Processing by SessionsController#create as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"z2IrGbXZvzHH5V2BtyjYNzqcJ5ZaHRtzCkCHEZ+O3EMDa8XOUjMtx672cjwUJS5hK3HEeLe0xH4PT/izupMhQQ==", "session"=>{"password"=>"[FILTERED]", "name"=>"aaa", "id"=>"1"}, "commit"=>"Entry"}
Can't verify CSRF token authenticity.
Completed 422 Unprocessable Entity in 1ms (ActiveRecord: 0.0ms)




ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken):

actionpack (5.1.4) lib/action_controller/metal/request_forgery_protection.rb:195:in `handle_unverified_request'
actionpack (5.1.4) lib/action_controller/metal/request_forgery_protection.rb:227:in `handle_unverified_request'
actionpack (5.1.4) lib/action_controller/metal/request_forgery_protection.rb:222:in `verify_authenticity_token'
...

Toggle session dump

_csrf_token: "31Er40wN1py9xD3U8EpSYDDpOd3VedSDfmVrKnsM7/o="
session_id: "dbe1f7f89fb50badb9136c7f86f4f93d"

原因分析

例外発生箇所特定

例外のコールスタック情報から発生箇所は以下

    private
      # The actual before_action that is used to verify the CSRF token.
      # Don't override this directly. Provide your own forgery protection
      # strategy instead. If you override, you'll disable same-origin
      # `<script>` verification.
      #
      # Lean on the protect_from_forgery declaration to mark which actions are
      # due for same-origin request verification. If protect_from_forgery is
      # enabled on an action, this before_action flags its after_action to
      # verify that JavaScript responses are for XHR requests, ensuring they
      # follow the browser's same-origin policy.
      def verify_authenticity_token # :doc:
        mark_for_same_origin_verification!

        if !verified_request?
          if logger && log_warning_on_csrf_failure
            if valid_request_origin?
              logger.warn "Can't verify CSRF token authenticity."
            else
              logger.warn "HTTP Origin header (#{request.origin}) didn't match request.base_url (#{request.base_url})"
            end
          end
          handle_unverified_request
        end
      end

ログにCan't verify CSRF token authenticity.と記録されていることから次のロジックです。

function or variable value about
verified_request false リクエストが有効か
valid_request_origin true originが一致している
logger true loggerが有効か判定
log_warning_on_csrf_failure true Warningを出力するフラグ

例外発生の原因はverified_requestfalseになったためです。

verified_requestがfalseになった原因

      # Returns true or false if a request is verified. Checks:
      #
      # * Is it a GET or HEAD request? GETs should be safe and idempotent
      # * Does the form_authenticity_token match the given token value from the params?
      # * Does the X-CSRF-Token header match the form_authenticity_token?
      def verified_request? # :doc:
        !protect_against_forgery? || request.get? || request.head? ||
          (valid_request_origin? && any_authenticity_token_valid?)
      end

ロジックを確認すると

function or variable value about
protect_against_forgery true CSRF対策が有効
request.get false GETリクエストではない
request.head false HEADリクエストではない
valid_request_origin true originが一致している
any_authenticity_token_valid false 認証トークンが全て無効

原因はany_authenticity_token_validfalseになったためです。

コメントによるとGET/HEADリクエスト以外のときに以下の2つがチェックされます。

  • form_authenticity_tokenparamsの値が一致しているか。
  • X-CSRF-Tokenヘッダの内容がform_authenticity_tokenと一致しているか。

form_authenticity_tokenは現在セッションのトークンを算出しているようです。

      # Sets the token value for the current session.
      def form_authenticity_token(form_options: {})
        masked_authenticity_token(session, form_options: form_options)
      end

any_authenticity_token_valid がfalseになった原因

request_authenticity_tokensに格納されたトークン全てをvalid_authenticity_tokenで走査しています。

      # Checks if any of the authenticity tokens from the request are valid.
      def any_authenticity_token_valid? # :doc:
        request_authenticity_tokens.any? do |token|
          valid_authenticity_token?(session, token)
        end
      end

request_authenticity_tokensの定義を確認すると、

      # Possible authenticity tokens sent in the request.
      def request_authenticity_tokens # :doc:
        [form_authenticity_param, request.x_csrf_token]
      end
      # The form's authenticity parameter. Override to provide your own.
      def form_authenticity_param # :doc:
        params[request_forgery_protection_token]
      end
  module RequestForgeryProtection
    extend ActiveSupport::Concern

    include AbstractController::Helpers
    include AbstractController::Callbacks

    included do
      # Sets the token parameter name for RequestForgery. Calling +protect_from_forgery+
      # sets it to <tt>:authenticity_token</tt> by default.
      config_accessor :request_forgery_protection_token
      self.request_forgery_protection_token ||= :authenticity_token

走査対象は以下の2つのようです。

  • params[:authenticity_token]
  • request.x_csrf_token

ログを見る限り前者しか値が入っていません

valid_authenticity_token?(session, token)がfalseとなる原因

encoded_masked_token = params[:authenticity_token]と想定して話をススメます。

      # Checks the client's masked token to see if it matches the
      # session token. Essentially the inverse of
      # +masked_authenticity_token+.
      def valid_authenticity_token?(session, encoded_masked_token) # :doc:
        if encoded_masked_token.nil? || encoded_masked_token.empty? || !encoded_masked_token.is_a?(String)
          return false
        end

        begin
          masked_token = Base64.strict_decode64(encoded_masked_token)
        rescue ArgumentError # encoded_masked_token is invalid Base64
          return false
        end

        # See if it's actually a masked token or not. In order to
        # deploy this code, we should be able to handle any unmasked
        # tokens that we've issued without error.

        if masked_token.length == AUTHENTICITY_TOKEN_LENGTH
          # This is actually an unmasked token. This is expected if
          # you have just upgraded to masked tokens, but should stop
          # happening shortly after installing this gem.
          compare_with_real_token masked_token, session

        elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2
          csrf_token = unmask_token(masked_token)

          compare_with_real_token(csrf_token, session) ||
            valid_per_form_csrf_token?(csrf_token, session)
        else
          false # Token is malformed.
        end
      end
      def compare_with_real_token(token, session) # :doc:
        ActiveSupport::SecurityUtils.secure_compare(token, real_csrf_token(session))
      end
      def real_csrf_token(session) # :doc:
        session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
        Base64.strict_decode64(session[:_csrf_token])
      end

まとめ

未だ原因不明

14
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
15