Skip to content

Commit 647dcc4

Browse files
authored
Merge pull request #395 from twitter/missing-directives
Add support for navigate-to, prefetch-src, and require-sri-for
2 parents 9965178 + 64a4fad commit 647dcc4

File tree

6 files changed

+82
-18
lines changed

6 files changed

+82
-18
lines changed

.travis.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@ language: ruby
22

33
rvm:
44
- ruby-head
5+
- 2.6.1
56
- 2.5.0
67
- 2.4.3
7-
- 2.3.6
8-
- 2.2.9
98
- jruby-head
109

1110
env:

lib/secure_headers/headers/content_security_policy.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,14 @@ def value
5151
def build_value
5252
directives.map do |directive_name|
5353
case DIRECTIVE_VALUE_TYPES[directive_name]
54+
when :source_list, :require_sri_for_list # require_sri is a simple set of strings that don't need to deal with symbol casing
55+
build_source_list_directive(directive_name)
5456
when :boolean
5557
symbol_to_hyphen_case(directive_name) if @config.directive_value(directive_name)
5658
when :sandbox_list
5759
build_sandbox_list_directive(directive_name)
5860
when :media_type_list
5961
build_media_type_list_directive(directive_name)
60-
when :source_list
61-
build_source_list_directive(directive_name)
6262
end
6363
end.compact.join("; ")
6464
end

lib/secure_headers/headers/content_security_policy_config.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,14 @@ def initialize(hash)
2727
@img_src = nil
2828
@manifest_src = nil
2929
@media_src = nil
30+
@navigate_to = nil
3031
@object_src = nil
3132
@plugin_types = nil
33+
@prefetch_src = nil
3234
@preserve_schemes = nil
3335
@report_only = nil
3436
@report_uri = nil
37+
@require_sri_for = nil
3538
@sandbox = nil
3639
@script_nonce = nil
3740
@script_src = nil

lib/secure_headers/headers/policy_management.rb

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
# frozen_string_literal: true
2+
3+
require "set"
4+
25
module SecureHeaders
36
module PolicyManagement
47
def self.included(base)
@@ -70,13 +73,19 @@ def self.included(base)
7073
# https://w3c.github.io/webappsec/specs/CSP2/
7174
BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content
7275
MANIFEST_SRC = :manifest_src
76+
NAVIGATE_TO = :navigate_to
77+
PREFETCH_SRC = :prefetch_src
78+
REQUIRE_SRI_FOR = :require_sri_for
7379
UPGRADE_INSECURE_REQUESTS = :upgrade_insecure_requests
7480
WORKER_SRC = :worker_src
7581

7682
DIRECTIVES_3_0 = [
7783
DIRECTIVES_2_0,
7884
BLOCK_ALL_MIXED_CONTENT,
7985
MANIFEST_SRC,
86+
NAVIGATE_TO,
87+
PREFETCH_SRC,
88+
REQUIRE_SRI_FOR,
8089
WORKER_SRC,
8190
UPGRADE_INSECURE_REQUESTS
8291
].flatten.freeze
@@ -100,14 +109,17 @@ def self.included(base)
100109
IMG_SRC => :source_list,
101110
MANIFEST_SRC => :source_list,
102111
MEDIA_SRC => :source_list,
112+
NAVIGATE_TO => :source_list,
103113
OBJECT_SRC => :source_list,
104114
PLUGIN_TYPES => :media_type_list,
115+
REQUIRE_SRI_FOR => :require_sri_for_list,
105116
REPORT_URI => :source_list,
117+
PREFETCH_SRC => :source_list,
106118
SANDBOX => :sandbox_list,
107119
SCRIPT_SRC => :source_list,
108120
STYLE_SRC => :source_list,
109121
WORKER_SRC => :source_list,
110-
UPGRADE_INSECURE_REQUESTS => :boolean
122+
UPGRADE_INSECURE_REQUESTS => :boolean,
111123
}.freeze
112124

113125
# These are directives that don't have use a source list, and hence do not
@@ -122,7 +134,8 @@ def self.included(base)
122134
BASE_URI,
123135
FORM_ACTION,
124136
FRAME_ANCESTORS,
125-
REPORT_URI
137+
NAVIGATE_TO,
138+
REPORT_URI,
126139
]
127140

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

164+
REQUIRE_SRI_FOR_VALUES = Set.new(%w(script style))
165+
151166
module ClassMethods
152167
# Public: generate a header name, value array that is user-agent-aware.
153168
#
@@ -241,7 +256,8 @@ def merge_policy_additions(original, additions)
241256
def list_directive?(directive)
242257
source_list?(directive) ||
243258
sandbox_list?(directive) ||
244-
media_type_list?(directive)
259+
media_type_list?(directive) ||
260+
require_sri_for_list?(directive)
245261
end
246262

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

293+
def require_sri_for_list?(directive)
294+
DIRECTIVE_VALUE_TYPES[directive] == :require_sri_for_list
295+
end
296+
277297
# Private: Validates that the configuration has a valid type, or that it is a valid
278298
# source expression.
279299
def validate_directive!(directive, value)
280300
ensure_valid_directive!(directive)
281301
case ContentSecurityPolicy::DIRECTIVE_VALUE_TYPES[directive]
302+
when :source_list
303+
validate_source_expression!(directive, value)
282304
when :boolean
283305
unless boolean?(value)
284306
raise ContentSecurityPolicyConfigError.new("#{directive} must be a boolean. Found #{value.class} value")
@@ -287,8 +309,8 @@ def validate_directive!(directive, value)
287309
validate_sandbox_expression!(directive, value)
288310
when :media_type_list
289311
validate_media_type_expression!(directive, value)
290-
when :source_list
291-
validate_source_expression!(directive, value)
312+
when :require_sri_for_list
313+
validate_require_sri_source_expression!(directive, value)
292314
else
293315
raise ContentSecurityPolicyConfigError.new("Unknown directive #{directive}")
294316
end
@@ -323,6 +345,16 @@ def validate_media_type_expression!(directive, media_type_expression)
323345
end
324346
end
325347

348+
# Private: validates that a require sri for expression:
349+
# 1. is an array of strings
350+
# 2. is a subset of ["string", "style"]
351+
def validate_require_sri_source_expression!(directive, require_sri_for_expression)
352+
ensure_array_of_strings!(directive, require_sri_for_expression)
353+
unless require_sri_for_expression.to_set.subset?(REQUIRE_SRI_FOR_VALUES)
354+
raise ContentSecurityPolicyConfigError.new(%(require-sri for must be a subset of #{REQUIRE_SRI_FOR_VALUES.to_a} but was #{require_sri_for_expression}))
355+
end
356+
end
357+
326358
# Private: validates that a source expression:
327359
# 1. is an array of strings
328360
# 2. does not contain any deprecated, now invalid values (inline, eval, self, none)

spec/lib/secure_headers/headers/content_security_policy_spec.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,31 @@ module SecureHeaders
116116
ContentSecurityPolicy.new(default_src: %w('self'), frame_src: %w('self')).value
117117
end
118118

119+
it "allows script as a require-sri-src" do
120+
csp = ContentSecurityPolicy.new(default_src: %w('self'), require_sri_for: %w(script))
121+
expect(csp.value).to eq("default-src 'self'; require-sri-for script")
122+
end
123+
124+
it "allows style as a require-sri-src" do
125+
csp = ContentSecurityPolicy.new(default_src: %w('self'), require_sri_for: %w(style))
126+
expect(csp.value).to eq("default-src 'self'; require-sri-for style")
127+
end
128+
129+
it "allows script and style as a require-sri-src" do
130+
csp = ContentSecurityPolicy.new(default_src: %w('self'), require_sri_for: %w(script style))
131+
expect(csp.value).to eq("default-src 'self'; require-sri-for script style")
132+
end
133+
134+
it "includes prefetch-src" do
135+
csp = ContentSecurityPolicy.new(default_src: %w('self'), prefetch_src: %w(foo.com))
136+
expect(csp.value).to eq("default-src 'self'; prefetch-src foo.com")
137+
end
138+
139+
it "includes navigate-to" do
140+
csp = ContentSecurityPolicy.new(default_src: %w('self'), navigate_to: %w(foo.com))
141+
expect(csp.value).to eq("default-src 'self'; navigate-to foo.com")
142+
end
143+
119144
it "supports strict-dynamic" do
120145
csp = ContentSecurityPolicy.new({default_src: %w('self'), script_src: [ContentSecurityPolicy::STRICT_DYNAMIC], script_nonce: 123456})
121146
expect(csp.value).to eq("default-src 'self'; script-src 'strict-dynamic' 'nonce-123456' 'unsafe-inline'")

spec/lib/secure_headers/headers/policy_management_spec.rb

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,24 +28,29 @@ module SecureHeaders
2828

2929
# directive values: these values will directly translate into source directives
3030
default_src: %w(https: 'self'),
31-
frame_src: %w('self' *.twimg.com itunes.apple.com),
32-
child_src: %w('self' *.twimg.com itunes.apple.com),
31+
32+
base_uri: %w('self'),
33+
block_all_mixed_content: true, # see [http://www.w3.org/TR/mixed-content/](http://www.w3.org/TR/mixed-content/)
3334
connect_src: %w(wss:),
35+
child_src: %w('self' *.twimg.com itunes.apple.com),
3436
font_src: %w('self' data:),
37+
form_action: %w('self' github.com),
38+
frame_ancestors: %w('none'),
39+
frame_src: %w('self' *.twimg.com itunes.apple.com),
3540
img_src: %w(mycdn.com data:),
3641
manifest_src: %w(manifest.com),
3742
media_src: %w(utoob.com),
43+
navigate_to: %w(netscape.com),
3844
object_src: %w('self'),
45+
plugin_types: %w(application/x-shockwave-flash),
46+
prefetch_src: %w(fetch.com),
47+
require_sri_for: %w(script style),
3948
script_src: %w('self'),
4049
style_src: %w('unsafe-inline'),
41-
worker_src: %w(worker.com),
42-
base_uri: %w('self'),
43-
form_action: %w('self' github.com),
44-
frame_ancestors: %w('none'),
45-
plugin_types: %w(application/x-shockwave-flash),
46-
block_all_mixed_content: true, # see [http://www.w3.org/TR/mixed-content/](http://www.w3.org/TR/mixed-content/)
4750
upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/
48-
report_uri: %w(https://example.com/uri-directive)
51+
worker_src: %w(worker.com),
52+
53+
report_uri: %w(https://example.com/uri-directive),
4954
}
5055

5156
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(config))

0 commit comments

Comments
 (0)