mirror of
https://github.com/basecamp/once-campfire.git
synced 2026-08-28 09:32:37 +09:00
Tokenize CSP_EXTRA_* semicolons to avoid a per-request 500
A per-request policy build (forced by the nonce) means a stray semicolon in a CSP_EXTRA_* value — a plausible operator paste like "https://youtube.com; https://vimeo.com" — lands inside a single source token and makes Rails raise InvalidDirectiveError on every request, a site-wide 500 even in report-only mode. Split on ';' alongside comma and whitespace so such a value tokenizes into valid sources. Rails still validates each token, so no injection is introduced (a token with an embedded space still fails validation).
This commit is contained in:
@@ -53,7 +53,8 @@ module CSP
|
||||
# these ENV knobs let an admin allow additional hosts without editing this file
|
||||
# (and without which enforcement would break their legitimate integrations).
|
||||
#
|
||||
# Each is a comma- or whitespace-separated list of CSP source expressions, e.g.
|
||||
# Each is a comma-, semicolon-, or whitespace-separated list of CSP source
|
||||
# expressions, e.g.
|
||||
#
|
||||
# CSP_EXTRA_FRAME_SRC="https://www.youtube.com https://player.vimeo.com"
|
||||
#
|
||||
@@ -68,8 +69,16 @@ module CSP
|
||||
}.freeze
|
||||
|
||||
# Parse one ENV knob into a list of extra host sources.
|
||||
#
|
||||
# Semicolons are tokenized like commas/whitespace: because the nonce forces a
|
||||
# per-request policy build, a stray `;` in a value (a plausible operator paste,
|
||||
# e.g. "https://youtube.com; https://vimeo.com") would otherwise land inside a
|
||||
# single source token and make Rails raise InvalidDirectiveError on every
|
||||
# request — a site-wide 500 even in report-only mode. Splitting on `;` yields
|
||||
# valid tokens instead; Rails still validates each one, so no injection is
|
||||
# introduced (a token with an embedded space still fails validation).
|
||||
def self.extra(directive)
|
||||
ENV[EXTRA_ENV.fetch(directive)].to_s.split(/[,\s]+/).reject(&:blank?)
|
||||
ENV[EXTRA_ENV.fetch(directive)].to_s.split(/[,;\s]+/).reject(&:blank?)
|
||||
end
|
||||
|
||||
# Build the baseline policy. Kept as a reusable method so it can be exercised
|
||||
|
||||
@@ -60,6 +60,27 @@ class CspNonceTest < ActionDispatch::IntegrationTest
|
||||
end
|
||||
end
|
||||
|
||||
test "a semicolon-separated ENV extra tokenizes into valid sources without raising" do
|
||||
# A plausible operator paste separates hosts with "; ". Because the nonce
|
||||
# forces a per-request policy build, a semicolon left inside a single source
|
||||
# token would make Rails raise InvalidDirectiveError on every request — a
|
||||
# site-wide 500 even in report-only mode. Splitting on ';' must yield both
|
||||
# hosts as valid tokens and never raise.
|
||||
with_env "CSP_EXTRA_FRAME_SRC" => "https://a.example; https://b.example" do
|
||||
header = nil
|
||||
assert_nothing_raised do
|
||||
header = ActionDispatch::ContentSecurityPolicy.new { |p| CSP.apply(p) }.build
|
||||
end
|
||||
|
||||
assert_match %r{frame-src[^;]*\bhttps://a\.example\b}, header
|
||||
assert_match %r{frame-src[^;]*\bhttps://b\.example\b}, header
|
||||
# Both hosts share the one frame-src directive; the semicolon did not leak
|
||||
# a second directive into the policy.
|
||||
assert_equal 1, header.scan(/(?:^|;\s*)frame-src\b/).size,
|
||||
"Expected exactly one frame-src directive"
|
||||
end
|
||||
end
|
||||
|
||||
test "directives default to :self only when no ENV extras are set" do
|
||||
with_env "CSP_EXTRA_FRAME_SRC" => nil, "CSP_EXTRA_IMG_SRC" => nil do
|
||||
header = ActionDispatch::ContentSecurityPolicy.new { |p| CSP.apply(p) }.build
|
||||
|
||||
Reference in New Issue
Block a user