跳到內容 跳到搜尋

Action Controller 請求偽造保護

控制器動作透過在應用程式的已呈現 HTML 中包含一個 token,以防止跨網站請求偽造 (CSRF) 攻擊。此 token 儲存在會話中,是一個隨機字串,攻擊者無法存取。當請求到達應用程式時,Rails 會驗證接收到的 token 與會話中的 token。除了 GET 請求之外,所有請求都會受到檢查,因為這些請求應該是冪等的。請記住,所有以會話為導向的請求預設都受到 CSRF 保護,包括 JavaScript 和 HTML 請求。

由於 HTML 和 JavaScript 請求通常是由瀏覽器發出的,因此我們需要確保驗證 Web 瀏覽器的請求真實性。我們可以使用會話導向驗證來處理這些類型的請求,方法是在控制器中使用 protect_from_forgery 方法。

GET 請求不受保護,因為它們沒有寫入資料庫等副作用,也不會洩漏敏感資訊。JavaScript 請求是一個例外:第三方網站可以使用 <script> 標籤來參考您網站上的 JavaScript URL。當您的 JavaScript 回應載入到他們的網站上時,就會執行。透過精心設計的 JavaScript,可能會擷取到 JavaScript 回應中的敏感資料。為防止這種情況,只有 XmlHttpRequest(稱為 XHR 或 Ajax)請求才能對 JavaScript 回應提出請求。

預設情況下,ActionController::Base 的子類別會受到 :exception 策略保護,此策略會對未驗證的請求引發 ActionController::InvalidAuthenticityToken 錯誤。

API 可能需要停用此行為,因為它們通常設計為無狀態:也就是說,請求 API 處理客戶端會處理會話,而不是 Rails。達成此目的的方法之一是使用 :null_session 策略,此策略允許處理未驗證的請求,但會話為空

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

請注意,API 應用程式預設不包含此模組或會話中介軟體,因此不需要設定 CSRF 保護。

預設令牌參數命名為 authenticity_token。此令牌的名稱和值必須新增到每個呈現表單的版面配置,方法是在 HTML head 中包含 csrf_meta_tags

Ruby on Rails 安全指南 中深入了解 CSRF 攻擊和保護應用程式。

命名空間
方法
A
C
F
G
H
M
N
P
R
U
V
X
包含模組

常數

AUTHENTICITY_TOKEN_LENGTH = 32
 
CSRF_TOKEN = "action_controller.csrf_token"
 
NULL_ORIGIN_MESSAGE = <<~MSG
 

類別公開方法

new(...)

# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 345
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 355
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 350
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 448
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 534
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 530
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 571
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 591
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 460
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 567
def global_csrf_token(session = nil) # :doc:
  csrf_token_hmac(session, GLOBAL_CSRF_TOKEN_IDENTIFIER)
end

handle_unverified_request()

# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 381
def handle_unverified_request # :doc:
  protection_strategy = forgery_protection_strategy.new(self)

  if protection_strategy.respond_to?(:warning_message)
    protection_strategy.warning_message = unverified_request_warning_message
  end

  protection_strategy.handle_unverified_request
end

mark_for_same_origin_verification!()

在渲染後,會檢查 GET 要求是否有跨來源 JavaScript。

# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 420
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 426
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 523
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 431
def non_xhr_javascript_response? # :doc:
  %r(\A(?:text|application)/javascript).match?(media_type) && !request.xhr?
end

normalize_action_path(action_path)

# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 621
def normalize_action_path(action_path) # :doc:
  uri = URI.parse(action_path)
  uri.path.chomp("/")
end

per_form_csrf_token(session, action_path, method)

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

protect_against_forgery?()

檢查控制器是否允許偽造防護。

# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 596
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 552
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 455
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 515
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)

檢查客戶端的遮罩權杖,以查看它是否與會話權杖相符。基本上是 masked_authenticity_token 的反向操作。

# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 483
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 = 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 538
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 611
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?()

如果請求已驗證,則傳回 true 或 false。檢查

  • 是 GET 或 HEAD 要求嗎?GET 應安全且冪等

  • form_authenticity_token 是否與參數中的給定令牌值相符?

  • X-CSRF-Token 標頭是否與 form_authenticity_token 相符?

# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 442
def verified_request? # :doc:
  !protect_against_forgery? || request.get? || request.head? ||
    (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 371
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 410
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 579
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