RailsのCSRF周りのコードリーディングをしました。
コードリーディングをする前に使い方を復習すると、protect_from_forgeryメソッドをコントローラで呼んで、csrf_meta_tagsのヘルパーでmetaタグを埋め込めばOKです。
<!DOCTYPE html>
<html>
<head>
<title>BasicRails</title>
<%= csrf_meta_tags %>
ActionController::RequestForgeryProtection::ClassMethods#protect_from_forgeryは以下のようになっています。
module ActionController #:nodoc:
module RequestForgeryProtection
module ClassMethods
def protect_from_forgery(options = {})
options = options.reverse_merge(prepend: false)
self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session)
self.request_forgery_protection_token ||= :authenticity_token
before_action :verify_authenticity_token, options
append_after_action :verify_same_origin_request
end
# ...
end
private
def form_authenticity_token(form_options: {})
masked_authenticity_token(session, form_options: form_options)
end
verify_authenticity_tokenはCSRFトークンを検証するbefore_actionのメソッド、クラス変数を操作しているのはCSRFのストラテジやトークンのパラメータ名になります。
ビューから呼び出せるcsrf_meta_tagsメソッドはActionView::Helpers::CsrfHelperのメソッドです。
module ActionView
module Helpers
module CsrfHelper
def csrf_meta_tags
if protect_against_forgery?
[
tag("meta", name: "csrf-param", content: request_forgery_protection_token),
tag("meta", name: "csrf-token", content: form_authenticity_token)
].join("\n").html_safe
end
end
#request_forgery_protection_tokenは:authenticity_token
がセットされます。
#form_authenticity_tokenは#masked_authenticity_tokenを呼び出します。
def masked_authenticity_token(session, form_options: {}) # :doc:
action, method = form_options.values_at(:action, :method)
raw_token = if per_form_csrf_tokens && action && method
action_path = normalize_action_path(action)
per_form_csrf_token(session, action_path, method)
else
real_csrf_token(session)
end
one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH)
encrypted_csrf_token = xor_byte_strings(one_time_pad, raw_token)
masked_token = one_time_pad + encrypted_csrf_token
Base64.strict_encode64(masked_token)
end
def real_csrf_token(session) # :doc:
session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
Base64.strict_decode64(session[:_csrf_token])
end
real_csrf_tokenはSecureRandomを使ってランダムなトークンを生成してセッションに格納します。one_time_padを生成して、csrf_tokenに対してXORを取って暗号化し、暗号化したトークンとone_time_padを連結したものをbase64エンコードしたものを返します。実体のsession[‘_csrf_token’]
はセッションごとに生成しつつ、ユーザに対して発行されるCSRFトークンはリクエスト毎に変わってくるため、SSLのBREACH攻撃を防ぐことができます。
CSRFトークン検証用の#verify_authenticity_tokenは#valid_authenticity_token?を呼び出します。上記で行ったマスキングの逆の操作を行い、CSRFトークンの検証を行います。
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
verifyに失敗した場合は#handle_unverified_request経由でforgery_protection_strategy.new(self).handle_unverified_request
が呼ばれます。#protect_from_forgeryのwithオプションに:exceptionを指定した場合は、forgery_protection_strategyはActionController::RequestForgeryProtection::ProtectionMethods::Exceptionになります。この場合、#handle_unverified_requestの処理はActionController::InvalidAuthenticityTokenのraiseになります。
module ActionController #:nodoc:
module RequestForgeryProtection
module ProtectionMethods
class Exception
def initialize(controller)
@controller = controller
end
def handle_unverified_request
raise ActionController::InvalidAuthenticityToken
end
end
end