システム
-
/
ページで部屋一覧を表示する。 - 部屋一覧ペーシから
/entry/:id
に移動して入室用ページを開く - 入室パスワードをFormから入力すると
/entry/
にPOSTする - POSTを受け取ると無条件に"Not Implemented"を表示する。
現象
POST時にSessionsController#create
でInvalidAuthenticityToken
例外が発生する。
ログ
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_request
がfalse
になったためです。
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_valid
がfalse
になったためです。
コメントによるとGET/HEADリクエスト以外のときに以下の2つがチェックされます。
-
form_authenticity_token
とparams
の値が一致しているか。 -
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
まとめ
未だ原因不明