secure_headers 3.4.0 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/CHANGELOG.md +75 -0
- data/Gemfile +1 -0
- data/README.md +54 -322
- data/docs/HPKP.md +17 -0
- data/docs/cookies.md +50 -0
- data/docs/hashes.md +64 -0
- data/docs/named_overrides_and_appends.md +107 -0
- data/docs/per_action_configuration.md +105 -0
- data/docs/sinatra.md +25 -0
- data/lib/secure_headers/configuration.rb +71 -37
- data/lib/secure_headers/headers/clear_site_data.rb +51 -0
- data/lib/secure_headers/headers/content_security_policy.rb +64 -37
- data/lib/secure_headers/headers/content_security_policy_config.rb +128 -0
- data/lib/secure_headers/headers/policy_management.rb +49 -30
- data/lib/secure_headers/headers/referrer_policy.rb +3 -0
- data/lib/secure_headers/view_helper.rb +8 -0
- data/lib/secure_headers.rb +133 -55
- data/secure_headers.gemspec +1 -1
- data/spec/lib/secure_headers/configuration_spec.rb +5 -5
- data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +103 -0
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +14 -2
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +25 -34
- data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +18 -0
- data/spec/lib/secure_headers/middleware_spec.rb +13 -1
- data/spec/lib/secure_headers/view_helpers_spec.rb +19 -6
- data/spec/lib/secure_headers_spec.rb +289 -40
- data/spec/spec_helper.rb +3 -2
- metadata +12 -2
data/lib/secure_headers.rb
CHANGED
|
@@ -10,22 +10,50 @@ require "secure_headers/headers/x_content_type_options"
|
|
|
10
10
|
require "secure_headers/headers/x_download_options"
|
|
11
11
|
require "secure_headers/headers/x_permitted_cross_domain_policies"
|
|
12
12
|
require "secure_headers/headers/referrer_policy"
|
|
13
|
+
require "secure_headers/headers/clear_site_data"
|
|
13
14
|
require "secure_headers/middleware"
|
|
14
15
|
require "secure_headers/railtie"
|
|
15
16
|
require "secure_headers/view_helper"
|
|
16
17
|
require "useragent"
|
|
18
|
+
require "singleton"
|
|
17
19
|
|
|
18
20
|
# All headers (except for hpkp) have a default value. Provide SecureHeaders::OPT_OUT
|
|
19
21
|
# or ":optout_of_protection" as a config value to disable a given header
|
|
20
22
|
module SecureHeaders
|
|
21
|
-
|
|
23
|
+
class NoOpHeaderConfig
|
|
24
|
+
include Singleton
|
|
25
|
+
|
|
26
|
+
def boom(arg = nil)
|
|
27
|
+
raise "Illegal State: attempted to modify NoOpHeaderConfig. Create a new config instead."
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_h
|
|
31
|
+
{}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def dup
|
|
35
|
+
self.class.instance
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def opt_out?
|
|
39
|
+
true
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
alias_method :[], :boom
|
|
43
|
+
alias_method :[]=, :boom
|
|
44
|
+
alias_method :keys, :boom
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
OPT_OUT = NoOpHeaderConfig.instance
|
|
22
48
|
SECURE_HEADERS_CONFIG = "secure_headers_request_config".freeze
|
|
23
49
|
NONCE_KEY = "secure_headers_content_security_policy_nonce".freeze
|
|
24
50
|
HTTPS = "https".freeze
|
|
25
51
|
CSP = ContentSecurityPolicy
|
|
26
52
|
|
|
27
53
|
ALL_HEADER_CLASSES = [
|
|
28
|
-
|
|
54
|
+
ClearSiteData,
|
|
55
|
+
ContentSecurityPolicyConfig,
|
|
56
|
+
ContentSecurityPolicyReportOnlyConfig,
|
|
29
57
|
StrictTransportSecurity,
|
|
30
58
|
PublicKeyPins,
|
|
31
59
|
ReferrerPolicy,
|
|
@@ -36,7 +64,10 @@ module SecureHeaders
|
|
|
36
64
|
XXssProtection
|
|
37
65
|
].freeze
|
|
38
66
|
|
|
39
|
-
ALL_HEADERS_BESIDES_CSP = (
|
|
67
|
+
ALL_HEADERS_BESIDES_CSP = (
|
|
68
|
+
ALL_HEADER_CLASSES -
|
|
69
|
+
[ContentSecurityPolicyConfig, ContentSecurityPolicyReportOnlyConfig]
|
|
70
|
+
).freeze
|
|
40
71
|
|
|
41
72
|
# Headers set on http requests (excludes STS and HPKP)
|
|
42
73
|
HTTP_HEADER_CLASSES =
|
|
@@ -50,13 +81,25 @@ module SecureHeaders
|
|
|
50
81
|
#
|
|
51
82
|
# additions - a hash containing directives. e.g.
|
|
52
83
|
# script_src: %w(another-host.com)
|
|
53
|
-
def override_content_security_policy_directives(request, additions)
|
|
54
|
-
config =
|
|
55
|
-
|
|
56
|
-
|
|
84
|
+
def override_content_security_policy_directives(request, additions, target = nil)
|
|
85
|
+
config, target = config_and_target(request, target)
|
|
86
|
+
|
|
87
|
+
if [:both, :enforced].include?(target)
|
|
88
|
+
if config.csp.opt_out?
|
|
89
|
+
config.csp = ContentSecurityPolicyConfig.new({})
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
config.csp.merge!(additions)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
if [:both, :report_only].include?(target)
|
|
96
|
+
if config.csp_report_only.opt_out?
|
|
97
|
+
config.csp_report_only = ContentSecurityPolicyReportOnlyConfig.new({})
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
config.csp_report_only.merge!(additions)
|
|
57
101
|
end
|
|
58
102
|
|
|
59
|
-
config.dynamic_csp = config.current_csp.merge(additions)
|
|
60
103
|
override_secure_headers_request_config(request, config)
|
|
61
104
|
end
|
|
62
105
|
|
|
@@ -66,12 +109,25 @@ module SecureHeaders
|
|
|
66
109
|
#
|
|
67
110
|
# additions - a hash containing directives. e.g.
|
|
68
111
|
# script_src: %w(another-host.com)
|
|
69
|
-
def append_content_security_policy_directives(request, additions)
|
|
70
|
-
config =
|
|
71
|
-
|
|
112
|
+
def append_content_security_policy_directives(request, additions, target = nil)
|
|
113
|
+
config, target = config_and_target(request, target)
|
|
114
|
+
|
|
115
|
+
if [:both, :enforced].include?(target) && !config.csp.opt_out?
|
|
116
|
+
config.csp.append(additions)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
if [:both, :report_only].include?(target) && !config.csp_report_only.opt_out?
|
|
120
|
+
config.csp_report_only.append(additions)
|
|
121
|
+
end
|
|
122
|
+
|
|
72
123
|
override_secure_headers_request_config(request, config)
|
|
73
124
|
end
|
|
74
125
|
|
|
126
|
+
def use_content_security_policy_named_append(request, name)
|
|
127
|
+
additions = SecureHeaders::Configuration.named_appends(name).call(request)
|
|
128
|
+
append_content_security_policy_directives(request, additions)
|
|
129
|
+
end
|
|
130
|
+
|
|
75
131
|
# Public: override X-Frame-Options settings for this request.
|
|
76
132
|
#
|
|
77
133
|
# value - deny, sameorigin, or allowall
|
|
@@ -107,12 +163,28 @@ module SecureHeaders
|
|
|
107
163
|
# returned is meant to be merged into the header value from `@app.call(env)`
|
|
108
164
|
# in Rack middleware.
|
|
109
165
|
def header_hash_for(request)
|
|
110
|
-
config = config_for(request)
|
|
111
|
-
|
|
112
|
-
|
|
166
|
+
config = config_for(request, prevent_dup = true)
|
|
167
|
+
headers = config.cached_headers
|
|
168
|
+
user_agent = UserAgent.parse(request.user_agent)
|
|
169
|
+
|
|
170
|
+
if !config.csp.opt_out? && config.csp.modified?
|
|
171
|
+
headers = update_cached_csp(config.csp, headers, user_agent)
|
|
113
172
|
end
|
|
114
173
|
|
|
115
|
-
|
|
174
|
+
if !config.csp_report_only.opt_out? && config.csp_report_only.modified?
|
|
175
|
+
headers = update_cached_csp(config.csp_report_only, headers, user_agent)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
header_classes_for(request).each_with_object({}) do |klass, hash|
|
|
179
|
+
if header = headers[klass::CONFIG_KEY]
|
|
180
|
+
header_name, value = if [ContentSecurityPolicyConfig, ContentSecurityPolicyReportOnlyConfig].include?(klass)
|
|
181
|
+
csp_header_for_ua(header, user_agent)
|
|
182
|
+
else
|
|
183
|
+
header
|
|
184
|
+
end
|
|
185
|
+
hash[header_name] = value
|
|
186
|
+
end
|
|
187
|
+
end
|
|
116
188
|
end
|
|
117
189
|
|
|
118
190
|
# Public: specify which named override will be used for this request.
|
|
@@ -133,7 +205,7 @@ module SecureHeaders
|
|
|
133
205
|
#
|
|
134
206
|
# Returns the nonce
|
|
135
207
|
def content_security_policy_script_nonce(request)
|
|
136
|
-
content_security_policy_nonce(request,
|
|
208
|
+
content_security_policy_nonce(request, ContentSecurityPolicy::SCRIPT_SRC)
|
|
137
209
|
end
|
|
138
210
|
|
|
139
211
|
# Public: gets or creates a nonce for CSP.
|
|
@@ -142,7 +214,7 @@ module SecureHeaders
|
|
|
142
214
|
#
|
|
143
215
|
# Returns the nonce
|
|
144
216
|
def content_security_policy_style_nonce(request)
|
|
145
|
-
content_security_policy_nonce(request,
|
|
217
|
+
content_security_policy_nonce(request, ContentSecurityPolicy::STYLE_SRC)
|
|
146
218
|
end
|
|
147
219
|
|
|
148
220
|
# Public: Retreives the config for a given header type:
|
|
@@ -150,11 +222,15 @@ module SecureHeaders
|
|
|
150
222
|
# Checks to see if there is an override for this request, then
|
|
151
223
|
# Checks to see if a named override is used for this request, then
|
|
152
224
|
# Falls back to the global config
|
|
153
|
-
def config_for(request)
|
|
225
|
+
def config_for(request, prevent_dup = false)
|
|
154
226
|
config = request.env[SECURE_HEADERS_CONFIG] ||
|
|
155
227
|
Configuration.get(Configuration::DEFAULT_CONFIG)
|
|
156
228
|
|
|
157
|
-
|
|
229
|
+
|
|
230
|
+
# Global configs are frozen, per-request configs are not. When we're not
|
|
231
|
+
# making modifications to the config, prevent_dup ensures we don't dup
|
|
232
|
+
# the object unnecessarily. It's not necessarily frozen to begin with.
|
|
233
|
+
if config.frozen? && !prevent_dup
|
|
158
234
|
config.dup
|
|
159
235
|
else
|
|
160
236
|
config
|
|
@@ -162,13 +238,38 @@ module SecureHeaders
|
|
|
162
238
|
end
|
|
163
239
|
|
|
164
240
|
private
|
|
241
|
+
TARGETS = [:both, :enforced, :report_only]
|
|
242
|
+
def raise_on_unknown_target(target)
|
|
243
|
+
unless TARGETS.include?(target)
|
|
244
|
+
raise "Unrecognized target: #{target}. Must be [:both, :enforced, :report_only]"
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def config_and_target(request, target)
|
|
249
|
+
config = config_for(request)
|
|
250
|
+
target = guess_target(config) unless target
|
|
251
|
+
raise_on_unknown_target(target)
|
|
252
|
+
[config, target]
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def guess_target(config)
|
|
256
|
+
if !config.csp.opt_out? && !config.csp_report_only.opt_out?
|
|
257
|
+
:both
|
|
258
|
+
elsif !config.csp.opt_out?
|
|
259
|
+
:enforced
|
|
260
|
+
elsif !config.csp_report_only.opt_out?
|
|
261
|
+
:report_only
|
|
262
|
+
else
|
|
263
|
+
:both
|
|
264
|
+
end
|
|
265
|
+
end
|
|
165
266
|
|
|
166
267
|
# Private: gets or creates a nonce for CSP.
|
|
167
268
|
#
|
|
168
269
|
# Returns the nonce
|
|
169
270
|
def content_security_policy_nonce(request, script_or_style)
|
|
170
271
|
request.env[NONCE_KEY] ||= SecureRandom.base64(32).chomp
|
|
171
|
-
nonce_key = script_or_style ==
|
|
272
|
+
nonce_key = script_or_style == ContentSecurityPolicy::SCRIPT_SRC ? :script_nonce : :style_nonce
|
|
172
273
|
append_content_security_policy_directives(request, nonce_key => request.env[NONCE_KEY])
|
|
173
274
|
request.env[NONCE_KEY]
|
|
174
275
|
end
|
|
@@ -193,21 +294,12 @@ module SecureHeaders
|
|
|
193
294
|
end
|
|
194
295
|
end
|
|
195
296
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if header = headers[klass::CONFIG_KEY]
|
|
203
|
-
header_name, value = if klass == CSP
|
|
204
|
-
csp_header_for_ua(header, request)
|
|
205
|
-
else
|
|
206
|
-
header
|
|
207
|
-
end
|
|
208
|
-
hash[header_name] = value
|
|
209
|
-
end
|
|
210
|
-
end
|
|
297
|
+
def update_cached_csp(config, headers, user_agent)
|
|
298
|
+
headers = Configuration.send(:deep_copy, headers)
|
|
299
|
+
headers[config.class::CONFIG_KEY] = {}
|
|
300
|
+
variation = ContentSecurityPolicy.ua_to_variation(user_agent)
|
|
301
|
+
headers[config.class::CONFIG_KEY][variation] = ContentSecurityPolicy.make_header(config, user_agent)
|
|
302
|
+
headers
|
|
211
303
|
end
|
|
212
304
|
|
|
213
305
|
# Private: chooses the applicable CSP header for the provided user agent.
|
|
@@ -215,26 +307,8 @@ module SecureHeaders
|
|
|
215
307
|
# headers - a hash of header_config_key => [header_name, header_value]
|
|
216
308
|
#
|
|
217
309
|
# Returns a CSP [header, value] array
|
|
218
|
-
def csp_header_for_ua(headers,
|
|
219
|
-
headers[
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
# Private: optionally build a header with a given configure
|
|
223
|
-
#
|
|
224
|
-
# klass - corresponding Class for a given header
|
|
225
|
-
# config - A string, symbol, or hash config for the header
|
|
226
|
-
# user_agent - A string representing the UA (only used for CSP feature sniffing)
|
|
227
|
-
#
|
|
228
|
-
# Returns a 2 element array [header_name, header_value] or nil if config
|
|
229
|
-
# is OPT_OUT
|
|
230
|
-
def make_header(klass, header_config, user_agent = nil)
|
|
231
|
-
unless header_config == OPT_OUT
|
|
232
|
-
if klass == CSP
|
|
233
|
-
klass.make_header(header_config, user_agent)
|
|
234
|
-
else
|
|
235
|
-
klass.make_header(header_config)
|
|
236
|
-
end
|
|
237
|
-
end
|
|
310
|
+
def csp_header_for_ua(headers, user_agent)
|
|
311
|
+
headers[ContentSecurityPolicy.ua_to_variation(user_agent)]
|
|
238
312
|
end
|
|
239
313
|
end
|
|
240
314
|
|
|
@@ -267,4 +341,8 @@ module SecureHeaders
|
|
|
267
341
|
def override_x_frame_options(value)
|
|
268
342
|
SecureHeaders.override_x_frame_options(request, value)
|
|
269
343
|
end
|
|
344
|
+
|
|
345
|
+
def use_content_security_policy_named_append(name)
|
|
346
|
+
SecureHeaders.use_content_security_policy_named_append(request, name)
|
|
347
|
+
end
|
|
270
348
|
end
|
data/secure_headers.gemspec
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
|
2
2
|
Gem::Specification.new do |gem|
|
|
3
3
|
gem.name = "secure_headers"
|
|
4
|
-
gem.version = "3.
|
|
4
|
+
gem.version = "3.6.0"
|
|
5
5
|
gem.authors = ["Neil Matatall"]
|
|
6
6
|
gem.email = ["neil.matatall@gmail.com"]
|
|
7
7
|
gem.description = 'Security related headers all in one gem.'
|
|
@@ -36,8 +36,8 @@ module SecureHeaders
|
|
|
36
36
|
|
|
37
37
|
config = Configuration.get(:test_override)
|
|
38
38
|
noop = Configuration.get(Configuration::NOOP_CONFIGURATION)
|
|
39
|
-
[:csp, :
|
|
40
|
-
expect(config.send(key)).to eq(noop.send(key))
|
|
39
|
+
[:csp, :csp_report_only, :cookies].each do |key|
|
|
40
|
+
expect(config.send(key)).to eq(noop.send(key))
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
43
|
|
|
@@ -65,7 +65,7 @@ module SecureHeaders
|
|
|
65
65
|
default = Configuration.get
|
|
66
66
|
override = Configuration.get(:override)
|
|
67
67
|
|
|
68
|
-
expect(override.csp).not_to
|
|
68
|
+
expect(override.csp.directive_value(:default_src)).not_to be(default.csp.directive_value(:default_src))
|
|
69
69
|
end
|
|
70
70
|
|
|
71
71
|
it "allows you to override an override" do
|
|
@@ -78,9 +78,9 @@ module SecureHeaders
|
|
|
78
78
|
end
|
|
79
79
|
|
|
80
80
|
original_override = Configuration.get(:override)
|
|
81
|
-
expect(original_override.csp).to eq(default_src: %w('self'))
|
|
81
|
+
expect(original_override.csp.to_h).to eq(default_src: %w('self'))
|
|
82
82
|
override_config = Configuration.get(:second_override)
|
|
83
|
-
expect(override_config.csp).to eq(default_src: %w('self'), script_src: %w(example.org))
|
|
83
|
+
expect(override_config.csp.to_h).to eq(default_src: %w('self'), script_src: %w('self' example.org))
|
|
84
84
|
end
|
|
85
85
|
|
|
86
86
|
it "deprecates the secure_cookies configuration" do
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
module SecureHeaders
|
|
4
|
+
describe ClearSiteData do
|
|
5
|
+
describe "make_header" do
|
|
6
|
+
it "returns nil with nil config" do
|
|
7
|
+
expect(described_class.make_header).to be_nil
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
it "returns nil with empty config" do
|
|
11
|
+
expect(described_class.make_header([])).to be_nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it "returns nil with opt-out config" do
|
|
15
|
+
expect(described_class.make_header(OPT_OUT)).to be_nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "returns all types with `true` config" do
|
|
19
|
+
name, value = described_class.make_header(true)
|
|
20
|
+
|
|
21
|
+
expect(name).to eq(ClearSiteData::HEADER_NAME)
|
|
22
|
+
expect(value).to eq(normalize_json(<<-HERE))
|
|
23
|
+
{
|
|
24
|
+
"types": [
|
|
25
|
+
"cache",
|
|
26
|
+
"cookies",
|
|
27
|
+
"storage",
|
|
28
|
+
"executionContexts"
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
HERE
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "returns specified types" do
|
|
35
|
+
name, value = described_class.make_header(["foo", "bar"])
|
|
36
|
+
|
|
37
|
+
expect(name).to eq(ClearSiteData::HEADER_NAME)
|
|
38
|
+
expect(value).to eq(normalize_json(<<-HERE))
|
|
39
|
+
{
|
|
40
|
+
"types": [
|
|
41
|
+
"foo",
|
|
42
|
+
"bar"
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
HERE
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
describe "validate_config!" do
|
|
50
|
+
it "succeeds for `true` config" do
|
|
51
|
+
expect do
|
|
52
|
+
described_class.validate_config!(true)
|
|
53
|
+
end.not_to raise_error
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "succeeds for `nil` config" do
|
|
57
|
+
expect do
|
|
58
|
+
described_class.validate_config!(nil)
|
|
59
|
+
end.not_to raise_error
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "succeeds for opt-out config" do
|
|
63
|
+
expect do
|
|
64
|
+
described_class.validate_config!(OPT_OUT)
|
|
65
|
+
end.not_to raise_error
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "succeeds for empty config" do
|
|
69
|
+
expect do
|
|
70
|
+
described_class.validate_config!([])
|
|
71
|
+
end.not_to raise_error
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it "succeeds for Array of Strings config" do
|
|
75
|
+
expect do
|
|
76
|
+
described_class.validate_config!(["foo"])
|
|
77
|
+
end.not_to raise_error
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "fails for Array of non-String config" do
|
|
81
|
+
expect do
|
|
82
|
+
described_class.validate_config!([1])
|
|
83
|
+
end.to raise_error(ClearSiteDataConfigError)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "fails for non-serializable config" do
|
|
87
|
+
expect do
|
|
88
|
+
described_class.validate_config!(["hi \255"])
|
|
89
|
+
end.to raise_error(ClearSiteDataConfigError)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "fails for other types of config" do
|
|
93
|
+
expect do
|
|
94
|
+
described_class.validate_config!(:cookies)
|
|
95
|
+
end.to raise_error(ClearSiteDataConfigError)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def normalize_json(json)
|
|
100
|
+
JSON.dump(JSON.parse(json))
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -14,11 +14,11 @@ module SecureHeaders
|
|
|
14
14
|
|
|
15
15
|
describe "#name" do
|
|
16
16
|
context "when in report-only mode" do
|
|
17
|
-
specify { expect(ContentSecurityPolicy.new(default_opts.merge(report_only: true)).name).to eq(
|
|
17
|
+
specify { expect(ContentSecurityPolicy.new(default_opts.merge(report_only: true)).name).to eq(ContentSecurityPolicyReportOnlyConfig::HEADER_NAME) }
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
context "when in enforce mode" do
|
|
21
|
-
specify { expect(ContentSecurityPolicy.new(default_opts).name).to eq(
|
|
21
|
+
specify { expect(ContentSecurityPolicy.new(default_opts).name).to eq(ContentSecurityPolicyConfig::HEADER_NAME) }
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
|
|
@@ -107,6 +107,11 @@ module SecureHeaders
|
|
|
107
107
|
expect(firefox_transitional).not_to match(/frame-src/)
|
|
108
108
|
end
|
|
109
109
|
|
|
110
|
+
it "supports strict-dynamic" do
|
|
111
|
+
csp = ContentSecurityPolicy.new({default_src: %w('self'), script_src: [ContentSecurityPolicy::STRICT_DYNAMIC], script_nonce: 123456}, USER_AGENTS[:chrome])
|
|
112
|
+
expect(csp.value).to eq("default-src 'self'; script-src 'strict-dynamic' 'nonce-123456'")
|
|
113
|
+
end
|
|
114
|
+
|
|
110
115
|
context "browser sniffing" do
|
|
111
116
|
let (:complex_opts) do
|
|
112
117
|
(ContentSecurityPolicy::ALL_DIRECTIVES - [:frame_src]).each_with_object({}) do |directive, hash|
|
|
@@ -149,6 +154,13 @@ module SecureHeaders
|
|
|
149
154
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari6])
|
|
150
155
|
expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox sandbox.com; script-src script-src.com 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
|
|
151
156
|
end
|
|
157
|
+
|
|
158
|
+
it "falls back to standard Firefox defaults when the useragent version is not present" do
|
|
159
|
+
ua = USER_AGENTS[:firefox].dup
|
|
160
|
+
allow(ua).to receive(:version).and_return(nil)
|
|
161
|
+
policy = ContentSecurityPolicy.new(complex_opts, ua)
|
|
162
|
+
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
|
|
163
|
+
end
|
|
152
164
|
end
|
|
153
165
|
end
|
|
154
166
|
end
|
|
@@ -40,70 +40,75 @@ module SecureHeaders
|
|
|
40
40
|
report_uri: %w(https://example.com/uri-directive)
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(config))
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
it "requires a :default_src value" do
|
|
47
47
|
expect do
|
|
48
|
-
|
|
48
|
+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(script_src: %('self')))
|
|
49
49
|
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
it "requires :report_only to be a truthy value" do
|
|
53
53
|
expect do
|
|
54
|
-
|
|
54
|
+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(report_only: "steve")))
|
|
55
55
|
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
it "requires :preserve_schemes to be a truthy value" do
|
|
59
59
|
expect do
|
|
60
|
-
|
|
60
|
+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(preserve_schemes: "steve")))
|
|
61
61
|
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
it "requires :block_all_mixed_content to be a boolean value" do
|
|
65
65
|
expect do
|
|
66
|
-
|
|
66
|
+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(block_all_mixed_content: "steve")))
|
|
67
67
|
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
it "requires :upgrade_insecure_requests to be a boolean value" do
|
|
71
71
|
expect do
|
|
72
|
-
|
|
72
|
+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(upgrade_insecure_requests: "steve")))
|
|
73
73
|
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
74
74
|
end
|
|
75
75
|
|
|
76
76
|
it "requires all source lists to be an array of strings" do
|
|
77
77
|
expect do
|
|
78
|
-
|
|
78
|
+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: "steve"))
|
|
79
79
|
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
it "allows nil values" do
|
|
83
83
|
expect do
|
|
84
|
-
|
|
84
|
+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w('self'), script_src: ["https:", nil]))
|
|
85
85
|
end.to_not raise_error
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
it "rejects unknown directives / config" do
|
|
89
89
|
expect do
|
|
90
|
-
|
|
90
|
+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w('self'), default_src_totally_mispelled: "steve"))
|
|
91
91
|
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
# this is mostly to ensure people don't use the antiquated shorthands common in other configs
|
|
95
95
|
it "performs light validation on source lists" do
|
|
96
96
|
expect do
|
|
97
|
-
|
|
97
|
+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w(self none inline eval)))
|
|
98
98
|
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
99
99
|
end
|
|
100
100
|
end
|
|
101
101
|
|
|
102
102
|
describe "#combine_policies" do
|
|
103
103
|
it "combines the default-src value with the override if the directive was unconfigured" do
|
|
104
|
-
|
|
104
|
+
Configuration.default do |config|
|
|
105
|
+
config.csp = {
|
|
106
|
+
default_src: %w(https:)
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, script_src: %w(anothercdn.com))
|
|
105
110
|
csp = ContentSecurityPolicy.new(combined_config)
|
|
106
|
-
expect(csp.name).to eq(
|
|
111
|
+
expect(csp.name).to eq(ContentSecurityPolicyConfig::HEADER_NAME)
|
|
107
112
|
expect(csp.value).to eq("default-src https:; script-src https: anothercdn.com")
|
|
108
113
|
end
|
|
109
114
|
|
|
@@ -115,7 +120,7 @@ module SecureHeaders
|
|
|
115
120
|
}.freeze
|
|
116
121
|
end
|
|
117
122
|
report_uri = "https://report-uri.io/asdf"
|
|
118
|
-
combined_config =
|
|
123
|
+
combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, report_uri: [report_uri])
|
|
119
124
|
csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox])
|
|
120
125
|
expect(csp.value).to include("report-uri #{report_uri}")
|
|
121
126
|
end
|
|
@@ -127,12 +132,12 @@ module SecureHeaders
|
|
|
127
132
|
report_only: false
|
|
128
133
|
}.freeze
|
|
129
134
|
end
|
|
130
|
-
non_default_source_additions =
|
|
135
|
+
non_default_source_additions = ContentSecurityPolicy::NON_FETCH_SOURCES.each_with_object({}) do |directive, hash|
|
|
131
136
|
hash[directive] = %w("http://example.org)
|
|
132
137
|
end
|
|
133
|
-
combined_config =
|
|
138
|
+
combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, non_default_source_additions)
|
|
134
139
|
|
|
135
|
-
|
|
140
|
+
ContentSecurityPolicy::NON_FETCH_SOURCES.each do |directive|
|
|
136
141
|
expect(combined_config[directive]).to eq(%w("http://example.org))
|
|
137
142
|
end
|
|
138
143
|
|
|
@@ -146,9 +151,9 @@ module SecureHeaders
|
|
|
146
151
|
report_only: false
|
|
147
152
|
}
|
|
148
153
|
end
|
|
149
|
-
combined_config =
|
|
154
|
+
combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, report_only: true)
|
|
150
155
|
csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox])
|
|
151
|
-
expect(csp.name).to eq(
|
|
156
|
+
expect(csp.name).to eq(ContentSecurityPolicyReportOnlyConfig::HEADER_NAME)
|
|
152
157
|
end
|
|
153
158
|
|
|
154
159
|
it "overrides the :block_all_mixed_content flag" do
|
|
@@ -158,7 +163,7 @@ module SecureHeaders
|
|
|
158
163
|
block_all_mixed_content: false
|
|
159
164
|
}
|
|
160
165
|
end
|
|
161
|
-
combined_config =
|
|
166
|
+
combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, block_all_mixed_content: true)
|
|
162
167
|
csp = ContentSecurityPolicy.new(combined_config)
|
|
163
168
|
expect(csp.value).to eq("default-src https:; block-all-mixed-content")
|
|
164
169
|
end
|
|
@@ -168,23 +173,9 @@ module SecureHeaders
|
|
|
168
173
|
config.csp = OPT_OUT
|
|
169
174
|
end
|
|
170
175
|
expect do
|
|
171
|
-
|
|
176
|
+
ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, script_src: %w(anothercdn.com))
|
|
172
177
|
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
173
178
|
end
|
|
174
179
|
end
|
|
175
|
-
|
|
176
|
-
describe "#idempotent_additions?" do
|
|
177
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?(OPT_OUT, script_src: %w(b.com))).to be false }
|
|
178
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(c.com))).to be false }
|
|
179
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: %w(b.com))).to be false }
|
|
180
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(a.com b.com c.com))).to be false }
|
|
181
|
-
|
|
182
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com))).to be true }
|
|
183
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w(b.com a.com))).to be true }
|
|
184
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: %w())).to be true }
|
|
185
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, script_src: [nil])).to be true }
|
|
186
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: [nil])).to be true }
|
|
187
|
-
specify { expect(ContentSecurityPolicy.idempotent_additions?({script_src: %w(a.com b.com)}, style_src: nil)).to be true }
|
|
188
|
-
end
|
|
189
180
|
end
|
|
190
181
|
end
|
|
@@ -18,6 +18,24 @@ module SecureHeaders
|
|
|
18
18
|
end.not_to raise_error
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
it "accepts 'same-origin'" do
|
|
22
|
+
expect do
|
|
23
|
+
ReferrerPolicy.validate_config!("same-origin")
|
|
24
|
+
end.not_to raise_error
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "accepts 'strict-origin'" do
|
|
28
|
+
expect do
|
|
29
|
+
ReferrerPolicy.validate_config!("strict-origin")
|
|
30
|
+
end.not_to raise_error
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "accepts 'strict-origin-when-cross-origin'" do
|
|
34
|
+
expect do
|
|
35
|
+
ReferrerPolicy.validate_config!("strict-origin-when-cross-origin")
|
|
36
|
+
end.not_to raise_error
|
|
37
|
+
end
|
|
38
|
+
|
|
21
39
|
it "accepts 'origin'" do
|
|
22
40
|
expect do
|
|
23
41
|
ReferrerPolicy.validate_config!("origin")
|