production 環境を local に構築、動作確認を試みたとき、csrf で引っかかった。
その時の備忘。
ポイントは action_controller
の request_forgery_protection.rb
の verified_request?
メソッド。
actionpack-5.0.3/lib/action_controller/metal/request_forgery_protection.rb
# 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
4つのメソッドを評価し、true/false を判定する。
先頭の3メソッドは、configやhttp method の判定なので、基本的には問題にならない。
ポイントは、後半の2つ。
valid_request_origin?
request.origin と request.base_url が等価であることを評価している。
actionpack-5.0.3/lib/action_controller/metal/request_forgery_protection.rb
# 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
前段に nginx を配置する場合、request の header に $scheme
をリレーする必要がある。
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Client-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; # この行
proxy_pass http://localhost:3000;
}
そして、前段に aws ELB が配置される場合は、以下$http_x_forwarded_proto
をリレーする。
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Client-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; # この行
proxy_pass http://localhost:3000;
}
}
any_authenticity_token_valid?
ここで、具体的にPOSTに付帯する token を評価する。
# Checks if any of the authenticity tokens from the request are valid.
def any_authenticity_token_valid?
request_authenticity_tokens.any? do |token|
valid_authenticity_token?(session, token)
end
end
# Possible authenticity tokens sent in the request.
def request_authenticity_tokens
[form_authenticity_param, request.x_csrf_token]
end
formのparams と header をそれぞれ評価する。
具体的な評価は、
- compare_with_real_token(csrf_token, session)
- valid_per_form_csrf_token?(csrf_token, session)
で実施する。
compare_with_real_token
session[:_csrf_token]にtokenを保管し、これと比較評価している。
def compare_with_real_token(token, session)
ActiveSupport::SecurityUtils.secure_compare(token, real_csrf_token(session))
end
def real_csrf_token(session)
session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
Base64.strict_decode64(session[:_csrf_token])
end
production環境のsession_storeで secure
を指定していると、httpで動作させた場合にsessionにsotreされなくなるので注意。
config/initializers/session_store.rb
Rails.application.config.session_store :cookie_store, key: '_truckers_session', secure: Rails.env.production?