Skip to content

Commit

Permalink
Add support for navigate-to, prefetch-src, and require-sri-for
Browse files Browse the repository at this point in the history
  • Loading branch information
oreoshake committed Nov 23, 2019
1 parent dbedc3c commit 02a9c4e
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 16 deletions.
4 changes: 2 additions & 2 deletions lib/secure_headers/headers/content_security_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,14 @@ 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
build_source_list_directive(directive_name)
when :boolean
symbol_to_hyphen_case(directive_name) if @config.directive_value(directive_name)
when :sandbox_list
build_sandbox_list_directive(directive_name)
when :media_type_list
build_media_type_list_directive(directive_name)
when :source_list
build_source_list_directive(directive_name)
end
end.compact.join("; ")
end
Expand Down
3 changes: 3 additions & 0 deletions lib/secure_headers/headers/content_security_policy_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,14 @@ def initialize(hash)
@img_src = nil
@manifest_src = nil
@media_src = nil
@navigate_to = nil
@object_src = nil
@plugin_types = nil
@prefetch_src = nil
@preserve_schemes = nil
@report_only = nil
@report_uri = nil
@require_sri_for = nil
@sandbox = nil
@script_nonce = nil
@script_src = nil
Expand Down
42 changes: 37 additions & 5 deletions lib/secure_headers/headers/policy_management.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# frozen_string_literal: true

require "set"

module SecureHeaders
module PolicyManagement
def self.included(base)
Expand Down Expand Up @@ -70,13 +73,19 @@ def self.included(base)
# https://w3c.github.io/webappsec/specs/CSP2/
BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content
MANIFEST_SRC = :manifest_src
NAVIGATE_TO = :navigate_to
PREFETCH_SRC = :prefetch_src
REQUIRE_SRI_FOR = :require_sri_for
UPGRADE_INSECURE_REQUESTS = :upgrade_insecure_requests
WORKER_SRC = :worker_src

DIRECTIVES_3_0 = [
DIRECTIVES_2_0,
BLOCK_ALL_MIXED_CONTENT,
MANIFEST_SRC,
NAVIGATE_TO,
PREFETCH_SRC,
REQUIRE_SRI_FOR,
WORKER_SRC,
UPGRADE_INSECURE_REQUESTS
].flatten.freeze
Expand All @@ -100,14 +109,17 @@ def self.included(base)
IMG_SRC => :source_list,
MANIFEST_SRC => :source_list,
MEDIA_SRC => :source_list,
NAVIGATE_TO => :source_list,
OBJECT_SRC => :source_list,
PLUGIN_TYPES => :media_type_list,
REQUIRE_SRI_FOR => :require_sri_for_list,
REPORT_URI => :source_list,
PREFETCH_SRC => :source_list,
SANDBOX => :sandbox_list,
SCRIPT_SRC => :source_list,
STYLE_SRC => :source_list,
WORKER_SRC => :source_list,
UPGRADE_INSECURE_REQUESTS => :boolean
UPGRADE_INSECURE_REQUESTS => :boolean,
}.freeze

# These are directives that don't have use a source list, and hence do not
Expand All @@ -122,7 +134,8 @@ def self.included(base)
BASE_URI,
FORM_ACTION,
FRAME_ANCESTORS,
REPORT_URI
NAVIGATE_TO,
REPORT_URI,
]

FETCH_SOURCES = ALL_DIRECTIVES - NON_FETCH_SOURCES - NON_SOURCE_LIST_SOURCES
Expand All @@ -148,6 +161,8 @@ def self.included(base)
:style_nonce
].freeze

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

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

# For each directive in additions that does not exist in the original config,
Expand Down Expand Up @@ -274,11 +290,17 @@ def media_type_list?(directive)
DIRECTIVE_VALUE_TYPES[directive] == :media_type_list
end

def require_sri_for_list?(directive)
DIRECTIVE_VALUE_TYPES[directive] == :require_sri_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)
ensure_valid_directive!(directive)
case ContentSecurityPolicy::DIRECTIVE_VALUE_TYPES[directive]
when :source_list
validate_source_expression!(directive, value)
when :boolean
unless boolean?(value)
raise ContentSecurityPolicyConfigError.new("#{directive} must be a boolean. Found #{value.class} value")
Expand All @@ -287,8 +309,8 @@ def validate_directive!(directive, value)
validate_sandbox_expression!(directive, value)
when :media_type_list
validate_media_type_expression!(directive, value)
when :source_list
validate_source_expression!(directive, value)
when :require_sri_for_list
validate_require_sri_source_expression!(directive, value)
else
raise ContentSecurityPolicyConfigError.new("Unknown directive #{directive}")
end
Expand Down Expand Up @@ -323,6 +345,16 @@ def validate_media_type_expression!(directive, media_type_expression)
end
end

# Private: validates that a require sri for expression:
# 1. is an array of strings
# 2. is a subset of ["string", "style"]
def validate_require_sri_source_expression!(directive, require_sri_for_expression)
ensure_array_of_strings!(directive, require_sri_for_expression)
unless require_sri_for_expression.to_set.subset?(REQUIRE_SRI_FOR_VALUES)
raise ContentSecurityPolicyConfigError.new(%(require-sri for must be a subset of #{REQUIRE_SRI_FOR_VALUES.to_a} but was #{require_sri_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
25 changes: 25 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 @@ -116,6 +116,31 @@ module SecureHeaders
ContentSecurityPolicy.new(default_src: %w('self'), frame_src: %w('self')).value
end

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

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

it "allows script and style as a require-sri-src" do
csp = ContentSecurityPolicy.new(default_src: %w('self'), require_sri_for: %w(script style))
expect(csp.value).to eq("default-src 'self'; require-sri-for script style")
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")
end

it "includes navigate-to" do
csp = ContentSecurityPolicy.new(default_src: %w('self'), navigate_to: %w(foo.com))
expect(csp.value).to eq("default-src 'self'; navigate-to foo.com")
end

it "supports strict-dynamic" do
csp = ContentSecurityPolicy.new({default_src: %w('self'), script_src: [ContentSecurityPolicy::STRICT_DYNAMIC], script_nonce: 123456})
expect(csp.value).to eq("default-src 'self'; script-src 'strict-dynamic' 'nonce-123456' 'unsafe-inline'")
Expand Down
23 changes: 14 additions & 9 deletions spec/lib/secure_headers/headers/policy_management_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,24 +28,29 @@ module SecureHeaders

# directive values: these values will directly translate into source directives
default_src: %w(https: 'self'),
frame_src: %w('self' *.twimg.com itunes.apple.com),
child_src: %w('self' *.twimg.com itunes.apple.com),

base_uri: %w('self'),
block_all_mixed_content: true, # see [http://www.w3.org/TR/mixed-content/](http://www.w3.org/TR/mixed-content/)
connect_src: %w(wss:),
child_src: %w('self' *.twimg.com itunes.apple.com),
font_src: %w('self' data:),
form_action: %w('self' github.com),
frame_ancestors: %w('none'),
frame_src: %w('self' *.twimg.com itunes.apple.com),
img_src: %w(mycdn.com data:),
manifest_src: %w(manifest.com),
media_src: %w(utoob.com),
navigate_to: %w(netscape.com),
object_src: %w('self'),
plugin_types: %w(application/x-shockwave-flash),
prefetch_src: %w(fetch.com),
require_sri_for: %w(script style),
script_src: %w('self'),
style_src: %w('unsafe-inline'),
worker_src: %w(worker.com),
base_uri: %w('self'),
form_action: %w('self' github.com),
frame_ancestors: %w('none'),
plugin_types: %w(application/x-shockwave-flash),
block_all_mixed_content: true, # see [http://www.w3.org/TR/mixed-content/](http://www.w3.org/TR/mixed-content/)
upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/
report_uri: %w(https://example.com/uri-directive)
worker_src: %w(worker.com),

report_uri: %w(https://example.com/uri-directive),
}

ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(config))
Expand Down

0 comments on commit 02a9c4e

Please sign in to comment.