151 lines
5.0 KiB
Ruby
151 lines
5.0 KiB
Ruby
require "test_helper"
|
|
|
|
class RequestForgeryProtectionTest < ActionDispatch::IntegrationTest
|
|
setup do
|
|
sign_in_as :kevin
|
|
|
|
# Forgery protection is disabled in test environment so we need to
|
|
# enable it here
|
|
@original_allow_forgery_protection = ActionController::Base.allow_forgery_protection
|
|
ActionController::Base.allow_forgery_protection = true
|
|
end
|
|
|
|
teardown do
|
|
ActionController::Base.allow_forgery_protection = @original_allow_forgery_protection
|
|
end
|
|
|
|
test "don't report when Sec-Fetch-Site is same-origin and CSRF token matches" do
|
|
assert_log(excludes: "CSRF protection check") do
|
|
post boards_path,
|
|
params: { board: { name: "Test Board" }, authenticity_token: csrf_token },
|
|
headers: { "Sec-Fetch-Site" => "same-origin" }
|
|
end
|
|
end
|
|
|
|
test "don't report when Sec-Fetch-Site is same-site and CSRF token matches" do
|
|
assert_log(excludes: "CSRF protection check") do
|
|
post boards_path,
|
|
params: { board: { name: "Test Board" }, authenticity_token: csrf_token },
|
|
headers: { "Sec-Fetch-Site" => "same-site" }
|
|
end
|
|
end
|
|
|
|
test "fail and report when token doesn't match, regardless of Sec-Fetch-Site" do
|
|
assert_report
|
|
|
|
assert_log(includes: ["CSRF protection check", "sec_fetch_site pass (same-origin)", "token fail"]) do
|
|
assert_no_difference -> { Board.count } do
|
|
post boards_path,
|
|
params: { board: { name: "Test Board" }, authenticity_token: "invalid-token" },
|
|
headers: { "Sec-Fetch-Site" => "same-origin" }
|
|
end
|
|
end
|
|
end
|
|
|
|
test "fail and report when Origin doesn't match, regardless of Sec-Fetch-Site" do
|
|
assert_report
|
|
|
|
assert_log(includes: ["CSRF protection check", "sec_fetch_site pass (same-origin)",
|
|
"token pass", "origin fail (evil-site.com)"]) do
|
|
assert_no_difference -> { Board.count } do
|
|
post boards_path,
|
|
params: { board: { name: "Test Board" }, authenticity_token: csrf_token },
|
|
headers: { "Sec-Fetch-Site" => "same-origin", Origin: "evil-site.com" }
|
|
end
|
|
end
|
|
end
|
|
|
|
test "succeed and report when Sec-Fetch-Site is cross-site and CSRF token matches" do
|
|
assert_report
|
|
|
|
assert_log(includes: ["CSRF protection check", "sec_fetch_site fail (cross-site)", "token pass"]) do
|
|
assert_difference -> { Board.count }, +1 do
|
|
post boards_path,
|
|
params: { board: { name: "Test Board" }, authenticity_token: csrf_token },
|
|
headers: { "Sec-Fetch-Site" => "cross-site" }
|
|
end
|
|
end
|
|
end
|
|
|
|
test "succeed and report when Sec-Fetch-Site is none" do
|
|
assert_report
|
|
|
|
assert_log(includes: ["CSRF protection check", "sec_fetch_site fail (none)", "token pass"]) do
|
|
assert_difference -> { Board.count }, +1 do
|
|
post boards_path,
|
|
params: { board: { name: "Test Board" }, authenticity_token: csrf_token },
|
|
headers: { "Sec-Fetch-Site" => "none" }
|
|
end
|
|
end
|
|
end
|
|
|
|
test "succeed and report when Sec-Fetch-Site is missing" do
|
|
assert_report
|
|
|
|
assert_log(includes: ["CSRF protection check", "sec_fetch_site fail ()", "token pass"]) do
|
|
assert_difference -> { Board.count }, +1 do
|
|
post boards_path, params: { board: { name: "Test Board" }, authenticity_token: csrf_token }
|
|
end
|
|
end
|
|
end
|
|
|
|
test "don't report and succeed for GET requests" do
|
|
assert_log(excludes: "CSRF protection check") do
|
|
get board_url(boards(:writebook)), headers: { "Sec-Fetch-Site" => "cross-site" }
|
|
assert_response :success
|
|
end
|
|
end
|
|
|
|
test "GET requests succeed regardless of Sec-Fetch-Site header" do
|
|
get board_url(boards(:writebook)), headers: { "Sec-Fetch-Site" => "cross-site" }
|
|
|
|
assert_response :success
|
|
end
|
|
|
|
test "appends Sec-Fetch-Site to existing Vary header" do
|
|
get board_url(boards(:writebook)), headers: { "Accept" => "text/html" }
|
|
|
|
assert_response :success
|
|
vary_values = response.headers["Vary"].split(",").map(&:strip)
|
|
assert_includes vary_values, "Sec-Fetch-Site"
|
|
end
|
|
|
|
test "appends Sec-Fetch-Site to Vary header on POST requests" do
|
|
post boards_path,
|
|
params: { board: { name: "Test Board" }, authenticity_token: csrf_token },
|
|
headers: { "Sec-Fetch-Site" => "same-origin" }
|
|
|
|
assert_response :redirect
|
|
assert_includes response.headers["Vary"], "Sec-Fetch-Site"
|
|
end
|
|
|
|
private
|
|
def assert_log(includes: [], excludes: [], &block)
|
|
original_logger = Rails.logger
|
|
log_output = StringIO.new
|
|
Rails.logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(log_output))
|
|
|
|
yield
|
|
|
|
Array(includes).each { assert_includes(log_output.string, it) }
|
|
Array(excludes).each { assert_not_includes(log_output.string, it) }
|
|
ensure
|
|
Rails.logger = original_logger
|
|
end
|
|
|
|
def assert_report
|
|
if Fizzy.saas?
|
|
Sentry.expects(:capture_message).with do |message, **kwargs|
|
|
message == "CSRF protection mismatch" && kwargs[:level] == :info
|
|
end
|
|
end
|
|
end
|
|
|
|
def csrf_token
|
|
@csrf_token ||= begin
|
|
get new_board_url
|
|
response.body[/name="authenticity_token" value="([^"]+)"/, 1]
|
|
end
|
|
end
|
|
end
|