модуль ActionController::RequestForgeryProtection
Защита контроллера Action от подделки запросов
Действия контроллера защищены от атак с подделкой межсайтовых запросов (CSRF) благодаря включению токена в HTML-код, отображаемый для вашего приложения. Этот токен хранится в сессии в виде случайной строки, к которой злоумышленник не имеет доступа. Когда запрос поступает в ваше приложение, Rails проверяет полученный токен, сверяя его с токеном в сессии. Проверяются все запросы, кроме запросов GET, поскольку они должны быть идемпотентными. Помните, что по умолчанию все запросы, связанные с сессией, защищены от CSRF, включая запросы JavaScript и HTML.
Поскольку запросы HTML и JavaScript обычно отправляются из браузера, необходимо проверять подлинность запросов браузера. Для таких типов запросов можно использовать аутентификацию на основе сессии с помощью метода protect_from_forgery в контроллерах.
Запросы GET не защищены, поскольку не имеют побочных эффектов, таких как запись в базу данных, и не раскрывают конфиденциальную информацию. Исключение составляют запросы JavaScript: сторонний сайт может использовать тег <script> для обращения к URL JavaScript на вашем сайте. Когда ответ JavaScript загружается на стороннем сайте, он выполняется. При тщательно подготовленном JavaScript на стороне злоумышленника из ответа JavaScript могут быть извлечены конфиденциальные данные. Чтобы предотвратить это, запрашивать ответы JavaScript разрешено только с помощью запросов XmlHttpRequest (известных как XHR или Ajax).
Подклассы 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. Имя и значение этого токена необходимо добавить в каждый макет, отображающий формы, включив csrf_meta_tags в HTML head.
Подробнее об атаках CSRF и защите приложения читайте в руководстве по безопасности Ruby on Rails.
Константы
- AUTHENTICITY_TOKEN_LENGTH
- CSRF_TOKEN
Открытые методы класса
# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 373 def initialize(...) super @_marked_for_same_origin_verification = nil end
Открытые методы экземпляра
# 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
# 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
Закрытые методы экземпляра
# 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 Проверяет, действителен ли какой-либо из токенов подлинности в запросе.
# 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
# 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
# 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 # File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 614 def form_authenticity_param # :doc: params[request_forgery_protection_token] end
Параметр подлинности формы. Переопределите метод, чтобы использовать собственный параметр.
# 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 Создаёт токен подлинности для текущего запроса.
# 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
# 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
После отображения проверяются запросы GET на наличие межсайтового JavaScript.
# 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
Если был выполнен before_action verify_authenticity_token, проверяет, что ответы JavaScript отправляются только в ответ на запросы GET с того же источника.
# 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
# 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
Проверяет ответы JavaScript на наличие межсайтовых запросов.
# 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 # 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
Проверяет, разрешена ли в контроллере защита от подделки запросов.
# 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 # 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
Возможные токены подлинности, переданные в запросе.
# 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
# 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 Проверяет, совпадает ли маскированный токен клиента с токеном сессии. По сути, это обратная операция для masked_authenticity_token.
# 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 # 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 Проверяет, поступил ли запрос с того же источника, анализируя заголовок Origin.
# 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 Возвращает 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 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 Это сам before_action, используемый для проверки токена CSRF. Не переопределяйте его напрямую. Вместо этого используйте собственную стратегию защиты от подделки запросов. При переопределении будет отключена проверка <script> с того же источника.
Используйте объявление protect_from_forgery, чтобы пометить действия, для которых требуется проверка запросов с того же источника. Если для действия включён protect_from_forgery, этот before_action помечает его after_action для проверки того, что ответы JavaScript предназначены для запросов XHR и соответствуют политике браузера для запросов с того же источника.
# 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 Если был выполнен verify_authenticity_token (это означает, что для данного запроса включена защита от подделки запросов), дополнительно проверяется, что не отправляется неавторизованный ответ с другого источника.
# 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
© 2004–2021 David Heinemeier Hansson
Licensed under the MIT License.