Files
fizzy/app/controllers/concerns/request_forgery_protection.rb
T
Rosa Gutierrez d88949288c Check and report on Sec-Fetch-Site header for forgery protection
This is a great, solid alternative to CSRF tokens for CSRF protection
when we aren't worried about older browsers or other kind of actors
doing modifying requests in our app, and could be a good test for future
upstreaming to Rails (although there we'd need to continue using CSRF
tokens or at least letting people opt out manually).

Let's start checking the header and reporting on it when CSRF fails or
when it doesn't match the other checks Rails does, and then promote this
to be the only way to defend from CSRF.
2025-11-25 19:19:50 +01:00

55 lines
1.6 KiB
Ruby

module RequestForgeryProtection
extend ActiveSupport::Concern
included do
after_action :append_set_fetch_site_to_vary_header
end
private
def append_set_fetch_site_to_vary_header
vary_header = response.headers["Vary"].to_s.split(",").map(&:strip).reject(&:blank?)
response.headers["Vary"] = (vary_header + [ "Sec-Fetch-Site" ]).join(",")
end
def verified_request?
return true if request.get? || request.head? || !protect_against_forgery?
origin = valid_request_origin?
token = any_authenticity_token_valid?
sec_fetch_site = safe_fetch_site?
report_on_forgery_protection_results(origin:, token:, sec_fetch_site:)
origin && token
end
SAFE_FETCH_SITES = %w[ same-origin same-site ]
def safe_fetch_site?
SAFE_FETCH_SITES.include?(safe_fetch_site_value)
end
def safe_fetch_site_value
request.headers["Sec-Fetch-Site"].to_s.downcase
end
def report_on_forgery_protection_results(origin:, token:, sec_fetch_site:)
unless [ origin, token, sec_fetch_site ].all?
info = {
origin: "#{pass_or_fail_value(origin)} (#{request.origin})",
token: "#{pass_or_fail_value(token)}",
sec_fetch_site: "#{pass_or_fail_value(sec_fetch_site)} (#{safe_fetch_site_value})"
}
Rails.logger.info "CSRF protection check: " + info.map { it.join(" ") }.join(", ")
if (origin && token) != sec_fetch_site
Sentry.capture_message "CSRF protection mismatch", level: :info, extra: { info: info }
end
end
end
def pass_or_fail_value(result)
result ? "pass" : "fail"
end
end