前提条件
- nginxがSSL Terminationをするリバースプロキシである
- Railsのバージョンが5の状態で
rails new
をしている
発生する問題
- formのPOST時に
Can't verify CSRF token authenticity.
というエラーが発生する
確認するところ
- CSRF Tokenがページ内に埋まっているか?
- nginxにおけるリパースプロキシの設定は間違っていないか?
CSRF Tokenがページ内に埋まっているか?
Railsで入力フォームを作成するときは、おそらくform_for
かform_tag
を使うことになると思います。その場合、普通はauthenticity_token
というhidden fieldが追加されています。
たとえばこれはQiitaの意見を送信するフォームにauthenticity_token
が埋め込まれている様子です。
またAjaxで送信するフォームなどは、代わりにcsrf_meta_tag
を<head>
の中で呼んでやることで、X_CSRF_Token
をリクエストヘッダーに付与することによってCSRF対策を行っています。
このように、Rails側にCSRF対策のためのtokenを送ることができているかを確認するといいでしょう。
たとえばこの記事を下書き保存したときのRequestをみてみると、このようにauthenticity_token
を送っているのが確認できます。これはChromeのdeveloper toolsにあるNetworkの欄で確認することができます。
nginxにおけるリバースプロキシの設定は間違っていないか?
Railsをproduction環境で動作させる場合、大抵はApacheやnginxをリバースプロキシとして前段に噛ませるでしょう。その場合に、リパースプロキシがRailsに送るheaderが間違っている(不足している)と、CSRF Tokenの検証に失敗します。
RailsがCSRF Tokenを検証する部分のコードを見てみます。
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します。
# 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?
はこうなっています。
# 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?
が先程述べたリパースプロキシの設定と関係してきます。
# 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
ですが、この設定値を見に行きます。
# 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対策はしっかり行いましょう。