Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add trusted-types and require-trusted-types-for CSP Directive #486

Merged
merged 10 commits into from
Aug 2, 2022
4 changes: 3 additions & 1 deletion lib/secure_headers/headers/content_security_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ def value
def build_value
directives.map do |directive_name|
case DIRECTIVE_VALUE_TYPES[directive_name]
when :source_list, :require_sri_for_list # require_sri is a simple set of strings that don't need to deal with symbol casing
when :source_list,
:require_sri_for_list, # require_sri is a simple set of strings that don't need to deal with symbol casing
:require_trusted_types_for_list
build_source_list_directive(directive_name)
when :boolean
symbol_to_hyphen_case(directive_name) if @config.directive_value(directive_name)
Expand Down
2 changes: 2 additions & 0 deletions lib/secure_headers/headers/content_security_policy_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def initialize(hash)
@report_only = nil
@report_uri = nil
@require_sri_for = nil
@require_trusted_types_for = nil
@sandbox = nil
@script_nonce = nil
@script_src = nil
Expand All @@ -44,6 +45,7 @@ def initialize(hash)
@style_src = nil
@style_src_elem = nil
@style_src_attr = nil
@trusted_types = nil
@worker_src = nil
@upgrade_insecure_requests = nil
@disable_nonce_backwards_compatibility = nil
Expand Down
36 changes: 34 additions & 2 deletions lib/secure_headers/headers/policy_management.rb
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,19 @@ def self.included(base)
STYLE_SRC_ATTR
].flatten.freeze

ALL_DIRECTIVES = (DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0).uniq.sort
# Experimental directives - these vary greatly in support
# See MDN for details.
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/trusted-types
TRUSTED_TYPES = :trusted_types
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/require-trusted-types-for
REQUIRE_TRUSTED_TYPES_FOR = :require_trusted_types_for

DIRECTIVES_EXPERIMENTAL = [
TRUSTED_TYPES,
REQUIRE_TRUSTED_TYPES_FOR,
].flatten.freeze

ALL_DIRECTIVES = (DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_EXPERIMENTAL).uniq.sort

# Think of default-src and report-uri as the beginning and end respectively,
# everything else is in between.
Expand All @@ -121,6 +133,7 @@ def self.included(base)
OBJECT_SRC => :source_list,
PLUGIN_TYPES => :media_type_list,
REQUIRE_SRI_FOR => :require_sri_for_list,
REQUIRE_TRUSTED_TYPES_FOR => :require_trusted_types_for_list,
REPORT_URI => :source_list,
PREFETCH_SRC => :source_list,
SANDBOX => :sandbox_list,
Expand All @@ -130,6 +143,7 @@ def self.included(base)
STYLE_SRC => :source_list,
STYLE_SRC_ELEM => :source_list,
STYLE_SRC_ATTR => :source_list,
TRUSTED_TYPES => :source_list,
WORKER_SRC => :source_list,
UPGRADE_INSECURE_REQUESTS => :boolean,
}.freeze
Expand Down Expand Up @@ -175,6 +189,7 @@ def self.included(base)
].freeze

REQUIRE_SRI_FOR_VALUES = Set.new(%w(script style))
REQUIRE_TRUSTED_TYPES_FOR_VALUES = Set.new(%w(script))

module ClassMethods
# Public: generate a header name, value array that is user-agent-aware.
Expand Down Expand Up @@ -270,7 +285,8 @@ def list_directive?(directive)
source_list?(directive) ||
sandbox_list?(directive) ||
media_type_list?(directive) ||
require_sri_for_list?(directive)
require_sri_for_list?(directive) ||
require_trusted_types_for_list?(directive)
end

# For each directive in additions that does not exist in the original config,
Expand Down Expand Up @@ -308,6 +324,10 @@ def require_sri_for_list?(directive)
DIRECTIVE_VALUE_TYPES[directive] == :require_sri_for_list
end

def require_trusted_types_for_list?(directive)
DIRECTIVE_VALUE_TYPES[directive] == :require_trusted_types_for_list
end

# Private: Validates that the configuration has a valid type, or that it is a valid
# source expression.
def validate_directive!(directive, value)
Expand All @@ -325,6 +345,8 @@ def validate_directive!(directive, value)
validate_media_type_expression!(directive, value)
when :require_sri_for_list
validate_require_sri_source_expression!(directive, value)
when :require_trusted_types_for_list
validate_require_trusted_types_for_source_expression!(directive, value)
else
raise ContentSecurityPolicyConfigError.new("Unknown directive #{directive}")
end
Expand Down Expand Up @@ -369,6 +391,16 @@ def validate_require_sri_source_expression!(directive, require_sri_for_expressio
end
end

# Private: validates that a require trusted types for expression:
# 1. is an array of strings
# 2. is a subset of ["script"]
def validate_require_trusted_types_for_source_expression!(directive, require_trusted_types_for_expression)
ensure_array_of_strings!(directive, require_trusted_types_for_expression)
unless require_trusted_types_for_expression.to_set.subset?(REQUIRE_TRUSTED_TYPES_FOR_VALUES)
raise ContentSecurityPolicyConfigError.new(%(require-trusted-types-for for must be a subset of #{REQUIRE_TRUSTED_TYPES_FOR_VALUES.to_a} but was #{require_trusted_types_for_expression}))
end
end

# Private: validates that a source expression:
# 1. is an array of strings
# 2. does not contain any deprecated, now invalid values (inline, eval, self, none)
Expand Down
20 changes: 20 additions & 0 deletions spec/lib/secure_headers/headers/content_security_policy_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@ module SecureHeaders
expect(csp.value).to eq("default-src 'self'; require-sri-for script style")
end

it "allows style as a require-trusted-types-for source" do
csp = ContentSecurityPolicy.new(default_src: %w('self'), require_trusted_types_for: %w(script))
expect(csp.value).to eq("default-src 'self'; require-trusted-types-for script")
end

it "includes prefetch-src" do
csp = ContentSecurityPolicy.new(default_src: %w('self'), prefetch_src: %w(foo.com))
expect(csp.value).to eq("default-src 'self'; prefetch-src foo.com")
Expand Down Expand Up @@ -185,6 +190,21 @@ module SecureHeaders
csp = ContentSecurityPolicy.new({style_src: %w('self'), style_src_attr: %w('self')})
expect(csp.value).to eq("style-src 'self'; style-src-attr 'self'")
end

it "supports trusted-types directive" do
csp = ContentSecurityPolicy.new({trusted_types: %w(blahblahpolicy)})
expect(csp.value).to eq("trusted-types blahblahpolicy")
end

it "supports trusted-types directive with 'none'" do
csp = ContentSecurityPolicy.new({trusted_types: %w(none)})
expect(csp.value).to eq("trusted-types none")
end

it "allows duplicate policy names in trusted-types directive" do
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it make sense to test 'none' explicitly as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, probably. I'll add that

csp = ContentSecurityPolicy.new({trusted_types: %w(blahblahpolicy 'allow-duplicates')})
expect(csp.value).to eq("trusted-types blahblahpolicy 'allow-duplicates'")
end
end
end
end
8 changes: 8 additions & 0 deletions spec/lib/secure_headers/headers/policy_management_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ module SecureHeaders
plugin_types: %w(application/x-shockwave-flash),
prefetch_src: %w(fetch.com),
require_sri_for: %w(script style),
require_trusted_types_for: %w(script),
script_src: %w('self'),
style_src: %w('unsafe-inline'),
upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/
Expand All @@ -53,6 +54,7 @@ module SecureHeaders
script_src_attr: %w(example.com),
style_src_elem: %w(example.com),
style_src_attr: %w(example.com),
trusted_types: %w(abcpolicy),

report_uri: %w(https://example.com/uri-directive),
}
Expand Down Expand Up @@ -120,6 +122,12 @@ module SecureHeaders
end.to raise_error(ContentSecurityPolicyConfigError)
end

it "rejects style for trusted types" do
expect do
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(style_src: %w('self'), require_trusted_types_for: %w(script style), trusted_types: %w(abcpolicy))))
end
end

# this is mostly to ensure people don't use the antiquated shorthands common in other configs
it "performs light validation on source lists" do
expect do
Expand Down