Skip to content

Commit 95a938d

Browse files
authored
normalize domains with trailing slashes (#477)
## All PRs: * [x] Has tests * [ ] Documentation updated - N/A ## Adding a new header Generally, adding a new header is always OK. * Is the header supported by any user agent? If so, which? * What does it do? * What are the valid values for the header? * Where does the specification live? ## Adding a new CSP directive * Is the directive supported by any user agent? If so, which? * What does it do? * What are the valid values for the directive? --- This PR normalises CSP source expressions to exclude trailing slashes from the Domain _if_ there is no other information in the path. CSP3 more explicitly calls this out in the [path match algorithm](https://www.w3.org/TR/CSP/#path-part-match): > If path A consists of one character that is equal to the U+002F > SOLIDUS character (/) and path B is empty, return "Matches". Also a URL like `example.com/foo` will match a source expression of `example.com`, as well as `example.com/`, so having two source expressions listed like this is redundant.
2 parents 5e9ec2e + a4a4b1b commit 95a938d

File tree

2 files changed

+31
-2
lines changed

2 files changed

+31
-2
lines changed

lib/secure_headers/headers/content_security_policy.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ def minify_source_list(directive, source_list)
129129
else
130130
source_list = populate_nonces(directive, source_list)
131131
source_list = reject_all_values_if_none(source_list)
132+
source_list = normalize_uri_paths(source_list)
132133

133134
unless directive == REPORT_URI || @preserve_schemes
134135
source_list = strip_source_schemes(source_list)
@@ -151,6 +152,26 @@ def reject_all_values_if_none(source_list)
151152
end
152153
end
153154

155+
def normalize_uri_paths(source_list)
156+
source_list.map do |source|
157+
# Normalize domains ending in a single / as without omitting the slash accomplishes the same.
158+
# https://www.w3.org/TR/CSP3/#match-paths § 6.6.2.10 Step 2
159+
begin
160+
uri = URI(source)
161+
if uri.path == "/"
162+
next source.chomp("/")
163+
end
164+
rescue URI::InvalidURIError
165+
end
166+
167+
if source.chomp("/").include?("/")
168+
source
169+
else
170+
source.chomp("/")
171+
end
172+
end
173+
end
174+
154175
# Private: append a nonce to the script/style directories if script_nonce
155176
# or style_nonce are provided.
156177
def populate_nonces(directive, source_list)

spec/lib/secure_headers/headers/content_security_policy_spec.rb

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,20 @@ module SecureHeaders
4848
expect(csp.value).to eq("default-src * 'unsafe-inline' 'unsafe-eval' data: blob:")
4949
end
5050

51+
it "normalizes source expressions that end with a trailing /" do
52+
config = {
53+
default_src: %w(a.example.org/ b.example.com/ wss://c.example.com/ c.example.net/foo/ b.example.co/bar wss://b.example.co/)
54+
}
55+
csp = ContentSecurityPolicy.new(config)
56+
expect(csp.value).to eq("default-src a.example.org b.example.com wss://c.example.com c.example.net/foo/ b.example.co/bar wss://b.example.co")
57+
end
58+
5159
it "does not minify source expressions based on overlapping wildcards" do
5260
config = {
53-
default_src: %w(a.example.org b.example.org *.example.org https://*.example.org)
61+
default_src: %w(a.example.org b.example.org *.example.org https://*.example.org c.example.org/)
5462
}
5563
csp = ContentSecurityPolicy.new(config)
56-
expect(csp.value).to eq("default-src a.example.org b.example.org *.example.org")
64+
expect(csp.value).to eq("default-src a.example.org b.example.org *.example.org c.example.org")
5765
end
5866

5967
it "removes http/s schemes from hosts" do

0 commit comments

Comments
 (0)