Help us understand the problem. What is going on with this article?

RailsのPOSTでCSRF TokenがVerifyできないときに確認したいこと

More than 3 years have passed since last update.

前提条件

  • nginxがSSL Terminationをするリバースプロキシである
  • Railsのバージョンが5の状態でrails newをしている

発生する問題

  • formのPOST時にCan't verify CSRF token authenticity.というエラーが発生する

確認するところ

  • CSRF Tokenがページ内に埋まっているか?
  • nginxにおけるリパースプロキシの設定は間違っていないか?

CSRF Tokenがページ内に埋まっているか?

Railsで入力フォームを作成するときは、おそらくform_forform_tagを使うことになると思います。その場合、普通はauthenticity_tokenというhidden fieldが追加されています。

たとえばこれはQiitaの意見を送信するフォームにauthenticity_tokenが埋め込まれている様子です。
qiita authenticity_token

またAjaxで送信するフォームなどは、代わりにcsrf_meta_tag<head>の中で呼んでやることで、X_CSRF_Tokenをリクエストヘッダーに付与することによってCSRF対策を行っています。

このように、Rails側にCSRF対策のためのtokenを送ることができているかを確認するといいでしょう。

たとえばこの記事を下書き保存したときのRequestをみてみると、このようにauthenticity_tokenを送っているのが確認できます。これはChromeのdeveloper toolsにあるNetworkの欄で確認することができます。
qiita save as draft request

nginxにおけるリバースプロキシの設定は間違っていないか?

Railsをproduction環境で動作させる場合、大抵はApacheやnginxをリバースプロキシとして前段に噛ませるでしょう。その場合に、リパースプロキシがRailsに送るheaderが間違っている(不足している)と、CSRF Tokenの検証に失敗します。

RailsがCSRF Tokenを検証する部分のコードを見てみます。

actionpack/lib/action_controller/metal/request_forgery_protection.rb#L211
      def verify_authenticity_token
        mark_for_same_origin_verification!

        if !verified_request?
          if logger && log_warning_on_csrf_failure
            logger.warn "Can't verify CSRF token authenticity."
          end
          handle_unverified_request
        end
      end

rails/request_forgery_protection.rb at v5.0.0.1 · rails/rails

まずmask_for_same_origin_verification!ですが、中身はこれです。要はGET Requestであればpassします。

actionpack/lib/action_controller/metal/request_forgery_protection.rb#L244
      # GET requests are checked for cross-origin JavaScript after rendering.
      def mark_for_same_origin_verification!
        @marked_for_same_origin_verification = request.get?
      end

rails/request_forgery_protection.rb at v5.0.0.1 · rails/rails

次のverified_request?はこうなっています。

action_controller/metal/request_forgery_protection.rb#L266
      # 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?
        !protect_against_forgery? || request.get? || request.head? ||
          (valid_request_origin? && any_authenticity_token_valid?)
      end

rails/request_forgery_protection.rb at v5.0.0.1 · rails/rails

protect_against_forgery?config.allow_forgery_protectionの設定値、つまりCSRFの検証をおこなうかどうかを返すメソッドで、デフォルトでtrueです。

その次の2つはhttp requestの種類をみています。GETもしくはHEADなら許可ということですね。

そして次のvalid_request_origin?が先程述べたリパースプロキシの設定と関係してきます。

actionpack/lib/action_controller/metal/request_forgery_protection.rb#L398
      # Checks if the request originated from the same origin by looking at the
      # Origin header.
      def valid_request_origin?
        if forgery_protection_origin_check
          # We accept blank origin headers because some user agents don't send it.
          request.origin.nil? || request.origin == request.base_url
        else
          true
        end
      end

rails/request_forgery_protection.rb at v5.0.0.1 · rails/rails

そしてこのforgery_protection_origin_checkですが、この設定値を見に行きます。

config/initializers/request_forgery_protection.rb
# Be sure to restart your server when you modify this file.

# Enable origin-checking CSRF mitigation.
Rails.application.config.action_controller.forgery_protection_origin_check = true

これはRails 5からデフォルトでtrueになった設定です。

ifの中でrequestのoriginとbase_urlが同一かどうかを検証していますが、リパースプロキシの設定に不足があるとこの検証が通らず、Can't verify CSRF token authenticity.となってしまいます。

僕の環境では、nginxの設定にproxy_set_header X-Forwarded-Proto $scheme;が不足していたためにこのエラーが発生していました。

location \ {
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header Host $http_host;
  proxy_set_header X-CSRF-Token $http_x_csrf_token;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Forwarded-Proto $scheme; #これがなかった
  proxy_redirect off;

}

まとめ

CSRF対策はしっかり行いましょう。

参考

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした