Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions lib/secure_headers/headers/content_security_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,10 @@ def reject_all_values_if_none(source_list)
# e.g. *.fd.fxlcf.dpdns.org asdf.github.com becomes *.fd.fxlcf.dpdns.org
def dedup_source_list(sources)
sources = sources.uniq
wild_sources = sources.select { |source| source =~ STAR_REGEXP }
wild_sources = sources.select { |source| source =~ DOMAIN_WILDCARD_REGEX || source =~ PORT_WILDCARD_REGEX }

if wild_sources.any?
schemes = sources.map { |source| [source, URI(source).scheme] }.to_h
schemes = sources.map { |source| [source, source_scheme(source)] }.to_h
sources.reject do |source|
!wild_sources.include?(source) &&
wild_sources.any? { |pattern| schemes[pattern] == schemes[source] && File.fnmatch(pattern, source) }
Expand Down Expand Up @@ -212,5 +212,13 @@ def strip_source_schemes(source_list)
def symbol_to_hyphen_case(sym)
sym.to_s.tr("_", "-")
end

def source_scheme(source)
uri = URI(source.sub(PORT_WILDCARD_REGEX, ""))
# If host is nil the given source doesn't contain a scheme
# e.g. for `example.org:443` it would return `example.org` as the scheme
# which is of course incorrect
uri.scheme if uri.host
Comment on lines +217 to +221
Copy link
Contributor

@lgarron lgarron Aug 16, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It turns out that this also fails for URLs like https://*.example.org:*

What do you think of hardcoding just HTTP and HTTPS to avoid further wildcard issues?

Suggested change
uri = URI(source.sub(PORT_WILDCARD_REGEX, ""))
# If host is nil the given source doesn't contain a scheme
# e.g. for `example.org:443` it would return `example.org` as the scheme
# which is of course incorrect
uri.scheme if uri.host
return "https" if source.start_with?("https://")
return "http" if source.start_with?("http://")
nil

Copy link
Contributor

@lgarron lgarron Aug 16, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, that will result in incorrect deduplication because of the code higher up. So I'd lean more towards just matching ^([A-Za-z0-9\-\+.]+):\/\/, which should return the correct scheme if a valid URL pattern is passed in.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I agree. I'll add a test case for that today and change it to the regex you suggested.

Copy link
Author

@machisuji machisuji Aug 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually having looked into it again I don't quite get what the issue is. Parsing https://*.example.org:* would indeed fail but this is why in line 217 uri = URI(source.sub(PORT_WILDCARD_REGEX, "")) it removes the port wildcard.

irb(main):003:0> PORT_WILDCARD_REGEX = /:\*/
=> /:\*/
irb(main):004:0> source = "https://*.example.org:*"
=> "https://*.example.org:*"
irb(main):005:0> URI(source.sub(PORT_WILDCARD_REGEX, ""))
=> #<URI::HTTPS https://*.example.org>
irb(main):006:0> _.scheme
=> "https"

So the source_scheme method does return the correct result for "https://*.example.org:*".

Copy link
Contributor

@lgarron lgarron Aug 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, I've tried to give this whole code a refactor at #498

end
end
end
3 changes: 2 additions & 1 deletion lib/secure_headers/headers/policy_management.rb
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ def self.included(base)

FETCH_SOURCES = ALL_DIRECTIVES - NON_FETCH_SOURCES - NON_SOURCE_LIST_SOURCES

STAR_REGEXP = Regexp.new(Regexp.escape(STAR))
DOMAIN_WILDCARD_REGEX = /(?<=\A|[^:])\*/
PORT_WILDCARD_REGEX = /:\*/
HTTP_SCHEME_REGEX = %r{\Ahttps?://}

WILDCARD_SOURCES = [
Expand Down
13 changes: 12 additions & 1 deletion spec/lib/secure_headers/headers/content_security_policy_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ module SecureHeaders
expect(csp.value).to eq("default-src *.example.org")
end

it "minifies overlapping port wildcards" do
csp = ContentSecurityPolicy.new(default_src: %w(example.org example.org:* example.org:443 https://example.org:80))
expect(csp.value).to eq("default-src example.org example.org:*")
end

it "allows for port wildcards" do
csp = ContentSecurityPolicy.new(connect_src: %w(ws://localhost:*))
expect(csp.value).to eq("connect-src ws://localhost:*")
end

it "removes http/s schemes from hosts" do
csp = ContentSecurityPolicy.new(default_src: %w(https://example.org))
expect(csp.value).to eq("default-src example.org")
Expand Down Expand Up @@ -102,7 +112,8 @@ module SecureHeaders
end

it "deduplicates any source expressions" do
csp = ContentSecurityPolicy.new(default_src: %w(example.org example.org example.org))
src = %w(example.org example.org http://example.org https://example.org)
csp = ContentSecurityPolicy.new(default_src: src)
expect(csp.value).to eq("default-src example.org")
end

Expand Down