модуль ActionController::RequestForgeryProtection
Защита от подделки межсайтовых запросов в контроллере
Действия контроллера защищены от атак типа Cross-Site Request Forgery (CSRF) путём включения токена в рендеренный HTML вашего приложения. Этот токен хранится как случайная строка в сессии, к которой злоумышленник не имеет доступа. Когда запрос достигает вашего приложения, Rails проверяет полученный токен с токеном в сессии. Все запросы проверяются, за исключением запросов GET, так как они должны быть идемпотентными. Имейте в виду, что все запросы, ориентированные на сессию, защищены от CSRF по умолчанию, включая запросы JavaScript и HTML.
Поскольку запросы HTML и JavaScript обычно выполняются из браузера, нам необходимо убедиться, что подлинность запроса проверяется для веб-браузера. Мы можем использовать сессионную аутентификацию для этих типов запросов, используя метод protect_from_forgery в наших контроллерах.
Запросы GET не защищены, так как они не имеют побочных эффектов, таких как запись в базу данных, и не раскрывают конфиденциальную информацию. Запросы JavaScript являются исключением: сторонний сайт может использовать тег <script> для ссылки на URL JavaScript на вашем сайте. Когда ваш ответ 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 приложения по умолчанию не включают этот модуль или middleware сессии, и поэтому не требуют настройки защиты от CSRF.
Параметр токена по умолчанию называется authenticity_token. Имя и значение этого токена должны быть добавлены в каждый макет, отображающий формы, включив csrf_meta_tags в HTML head.
Узнайте больше об атаках CSRF и защите вашего приложения в Руководстве по безопасности Ruby on Rails.
Константы
- AUTHENTICITY_TOKEN_LENGTH
- CSRF_TOKEN
- NULL_ORIGIN_MESSAGE
Публичные методы класса
# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 364 def initialize(...) super @_marked_for_same_origin_verification = nil end
Публичные методы экземпляра
# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 374 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 369 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 467
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 548 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 544 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 585
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 605 def form_authenticity_param # :doc: params[request_forgery_protection_token] end
Параметр подлинности формы. Переопределите, чтобы использовать свой собственный.
# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 479
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 581 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 437 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 443 def marked_for_same_origin_verification? # :doc: @_marked_for_same_origin_verification ||= false end
Если verify_authenticity_token before_action был запущен, проверяется, что ответы JavaScript предоставляются только запросам GET с одного источника.
# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 537 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 448 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 635
def normalize_action_path(action_path) # :doc:
uri = URI.parse(action_path)
if uri.relative? && (action_path.blank? || !action_path.start_with?("/"))
normalize_relative_action_path(uri.path)
else
uri.path.chomp("/")
end
end # File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 645
def normalize_relative_action_path(rel_action_path) # :doc:
uri = URI.parse(request.path)
# add the action path to the request.path
uri.path += "/#{rel_action_path}"
# relative path with "./path"
uri.path.gsub!("/./", "/")
uri.path.chomp("/")
end # File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 574
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 610 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 566
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 474 def request_authenticity_tokens # :doc: [form_authenticity_param, request.x_csrf_token] end
Возможные токены подлинности, отправленные в запросе.
# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 530 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 500
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 Проверяет замаскированный токен клиента на соответствие сессионному токену. По сути, обратная функция masked_authenticity_token.
# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 552
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 625
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 461
def verified_request? # :doc:
!protect_against_forgery? || request.get? || request.head? ||
(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 389
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 427
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 593
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.