d88949288c
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.
55 lines
1.6 KiB
Ruby
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
|