跳至内容 跳至搜索

Action Controller 请求伪造防护

通过在应用程序的渲染 HTML 中包含一个令牌,可以保护 Controller 操作免受跨站请求伪造 (CSRF) 攻击。此令牌作为随机字符串存储在 session 中,攻击者无法访问。当请求到达您的应用程序时,Rails 会将收到的令牌与 session 中的令牌进行验证。所有请求都会被检查,GET 请求除外,因为 GET 请求应该是幂等的。请注意,默认情况下,所有面向 session 的请求都受到 CSRF 保护,包括 JavaScript 和 HTML 请求。

由于 HTML 和 JavaScript 请求通常来自浏览器,因此我们需要确保验证 Web 浏览器的请求真实性。我们可以通过在 Controller 中使用 protect_from_forgery 方法来实现这些请求的面向 session 的身份验证。

GET 请求不受保护,因为它们没有副作用,例如写入数据库,也不会泄露敏感信息。JavaScript 请求是一个例外:第三方网站可以使用 <script> 标签引用您网站上的 JavaScript URL。当您的 JavaScript 响应在其网站上加载时,它就会执行。通过精心设计的 JavaScript,您 JavaScript 响应中的敏感数据可能会被提取。为防止这种情况发生,只允许 XmlHttpRequest (即 XHR 或 Ajax) 请求访问 JavaScript 响应。

默认情况下,ActionController::Base 的子类受到 :exception 策略的保护,该策略会在未经验证的请求上引发 ActionController::InvalidAuthenticityToken 错误。

API 可能希望禁用此行为,因为它们通常设计为无状态的:即,请求 API 客户端由 Rails 来处理 session。实现此目的的一种方法是改用 :null_session 策略,该策略允许处理未经验证的请求,但 session 为空。

class ApplicationController < ActionController::Base
  protect_from_forgery with: :null_session
end

请注意,纯 API 应用程序默认不包含此模块或 session 中间件,因此不需要配置 CSRF 保护。

令牌参数默认命名为 authenticity_token。必须通过在 HTML head 中包含 csrf_meta_tags 来将此令牌的名称和值添加到渲染表单的每个布局中。

Ruby on Rails 安全指南 中了解更多关于 CSRF 攻击和保护应用程序的信息。

命名空间
方法
A
C
F
G
M
N
P
R
U
V
X
包含的模块

常量

AUTHENTICITY_TOKEN_LENGTH = 32
 
CSRF_TOKEN = "action_controller.csrf_token"
 

类公共方法

new(...)

# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 373
def initialize(...)
  super
  @_marked_for_same_origin_verification = nil
end

实例公共方法

commit_csrf_token(request)

# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 383
def commit_csrf_token(request) # :doc:
  csrf_token = request.env[CSRF_TOKEN]
  csrf_token_storage_strategy.store(request, csrf_token) unless csrf_token.nil?
end

reset_csrf_token(request)

# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 378
def reset_csrf_token(request) # :doc:
  request.env.delete(CSRF_TOKEN)
  csrf_token_storage_strategy.reset(request)
end

实例私有方法

any_authenticity_token_valid?()

检查请求中的任何身份验证令牌是否有效。

# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 476
def any_authenticity_token_valid? # :doc:
  request_authenticity_tokens.any? do |token|
    valid_authenticity_token?(session, token)
  end
end

compare_with_global_token(token, session = nil)

# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 557
def compare_with_global_token(token, session = nil) # :doc:
  ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, global_csrf_token(session))
end

compare_with_real_token(token, session = nil)

# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 553
def compare_with_real_token(token, session = nil) # :doc:
  ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, real_csrf_token(session))
end

csrf_token_hmac(session, identifier)

# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 594
def csrf_token_hmac(session, identifier) # :doc:
  OpenSSL::HMAC.digest(
    OpenSSL::Digest::SHA256.new,
    real_csrf_token(session),
    identifier
  )
end

form_authenticity_param()

表单的真实性参数。覆盖以提供您自己的参数。

# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 614
def form_authenticity_param # :doc:
  params[request_forgery_protection_token]
end

form_authenticity_token(form_options: {})

创建当前请求的真实性令牌。

# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 488
def form_authenticity_token(form_options: {}) # :doc:
  masked_authenticity_token(form_options: form_options)
end

global_csrf_token(session = nil)

# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 590
def global_csrf_token(session = nil) # :doc:
  csrf_token_hmac(session, GLOBAL_CSRF_TOKEN_IDENTIFIER)
end

mark_for_same_origin_verification!()

GET 请求将在渲染后检查跨域 JavaScript。

# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 446
def mark_for_same_origin_verification! # :doc:
  @_marked_for_same_origin_verification = request.get?
end

marked_for_same_origin_verification?()

如果 verify_authenticity_token before_action 已运行,则验证 JavaScript 响应是否仅提供给同源 GET 请求。

# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 452
def marked_for_same_origin_verification? # :doc:
  @_marked_for_same_origin_verification ||= false
end

mask_token(raw_token)

# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 546
def mask_token(raw_token) # :doc:
  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
  encode_csrf_token(masked_token)
end

non_xhr_javascript_response?()

检查跨域 JavaScript 响应。

# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 457
def non_xhr_javascript_response? # :doc:
  %r(\A(?:text|application)/javascript).match?(media_type) && !request.xhr?
end

per_form_csrf_token(session, action_path, method)

# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 583
def per_form_csrf_token(session, action_path, method) # :doc:
  csrf_token_hmac(session, [action_path, method.downcase].join("#"))
end

protect_against_forgery?()

检查 Controller 是否允许伪造保护。

# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 619
def protect_against_forgery? # :doc:
  allow_forgery_protection && (!session.respond_to?(:enabled?) || session.enabled?)
end

real_csrf_token(_session = nil)

# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 575
def real_csrf_token(_session = nil) # :doc:
  csrf_token = request.env.fetch(CSRF_TOKEN) do
    request.env[CSRF_TOKEN] = csrf_token_storage_strategy.fetch(request) || generate_csrf_token
  end

  decode_csrf_token(csrf_token)
end

request_authenticity_tokens()

请求中可能存在的真实性令牌。

# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 483
def request_authenticity_tokens # :doc:
  [form_authenticity_param, request.x_csrf_token]
end

unmask_token(masked_token)

# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 539
def unmask_token(masked_token) # :doc:
  # Split the token into the one-time pad and the encrypted value and decrypt it.
  one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH]
  encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1]
  xor_byte_strings(one_time_pad, encrypted_csrf_token)
end

valid_authenticity_token?(session, encoded_masked_token)

检查客户端的掩码令牌是否与 session 令牌匹配。本质上是 masked_authenticity_token 的反向操作。

# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 509
def valid_authenticity_token?(session, encoded_masked_token) # :doc:
  if !encoded_masked_token.is_a?(String) || encoded_masked_token.empty?
    return false
  end

  begin
    masked_token = decode_csrf_token(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

  elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2
    csrf_token = unmask_token(masked_token)

    compare_with_global_token(csrf_token) ||
      compare_with_real_token(csrf_token) ||
      valid_per_form_csrf_token?(csrf_token)
  else
    false # Token is malformed.
  end
end

valid_per_form_csrf_token?(token, session = nil)

# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 561
def valid_per_form_csrf_token?(token, session = nil) # :doc:
  if per_form_csrf_tokens
    correct_token = per_form_csrf_token(
      session,
      request.path.chomp("/"),
      request.request_method
    )

    ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, correct_token)
  else
    false
  end
end

valid_request_origin?()

通过检查 Origin 标头来判断请求是否源自同一来源。

# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 635
def valid_request_origin? # :doc:
  if forgery_protection_origin_check
    # We accept blank origin headers because some user agents don't send it.
    raise InvalidAuthenticityToken, NULL_ORIGIN_MESSAGE if request.origin == "null"
    request.origin.nil? || request.origin == request.base_url
  else
    true
  end
end

verified_request?()

返回请求是否已验证。检查:

  • 它是 GET 或 HEAD 请求吗?GET 请求应该是安全且幂等的。

  • form_authenticity_token 是否与 params 中给定的令牌值匹配?

  • X-CSRF-Token 标头是否与 form_authenticity_token 匹配?

# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 470
def verified_request? # :doc:
  request.get? || request.head? || !protect_against_forgery? ||
    (valid_request_origin? && any_authenticity_token_valid?)
end

verify_authenticity_token()

用于验证 CSRF 令牌的实际 before_action。不要直接覆盖此方法。而是提供您自己的伪造保护策略。如果您覆盖了此方法,您将禁用同源 <script> 验证。

依赖 protect_from_forgery 声明来标记哪些操作需要进行同源请求验证。如果 protect_from_forgery 在操作上启用,此 before_action 将标记其 after_action 以验证 JavaScript 响应是否是 XHR 请求,从而确保它们遵循浏览器的同源策略。

# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 398
def verify_authenticity_token # :doc:
  mark_for_same_origin_verification!

  if !verified_request?
    logger.warn unverified_request_warning_message if logger && log_warning_on_csrf_failure

    handle_unverified_request
  end
end

verify_same_origin_request()

如果 verify_authenticity_token 已运行(表明此请求已启用伪造保护),则还要验证我们没有提供未经授权的跨域响应。

# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 436
def verify_same_origin_request # :doc:
  if marked_for_same_origin_verification? && non_xhr_javascript_response?
    if logger && log_warning_on_csrf_failure
      logger.warn CROSS_ORIGIN_JAVASCRIPT_WARNING
    end
    raise ActionController::InvalidCrossOriginRequest, CROSS_ORIGIN_JAVASCRIPT_WARNING
  end
end

xor_byte_strings(s1, s2)

# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 602
def xor_byte_strings(s1, s2) # :doc:
  s2 = s2.dup
  size = s1.bytesize
  i = 0
  while i < size
    s2.setbyte(i, s1.getbyte(i) ^ s2.getbyte(i))
    i += 1
  end
  s2
end