secure_headers 3.4.1 → 3.5.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 +5 -0
- data/Gemfile +1 -0
- data/README.md +31 -7
- data/lib/secure_headers/configuration.rb +57 -36
- data/lib/secure_headers/headers/content_security_policy.rb +62 -36
- data/lib/secure_headers/headers/content_security_policy_config.rb +128 -0
- data/lib/secure_headers/headers/policy_management.rb +19 -18
- data/lib/secure_headers/view_helper.rb +8 -0
- data/lib/secure_headers.rb +122 -55
- data/secure_headers.gemspec +1 -1
- data/spec/lib/secure_headers/configuration_spec.rb +5 -5
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +2 -2
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +25 -34
- 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 +250 -58
- data/spec/spec_helper.rb +3 -2
- metadata +3 -2
|
@@ -7,9 +7,6 @@ module SecureHeaders
|
|
|
7
7
|
MODERN_BROWSERS = %w(Chrome Opera Firefox)
|
|
8
8
|
DEFAULT_VALUE = "default-src https:".freeze
|
|
9
9
|
DEFAULT_CONFIG = { default_src: %w(https:) }.freeze
|
|
10
|
-
HEADER_NAME = "Content-Security-Policy".freeze
|
|
11
|
-
REPORT_ONLY = "Content-Security-Policy-Report-Only".freeze
|
|
12
|
-
HEADER_NAMES = [HEADER_NAME, REPORT_ONLY]
|
|
13
10
|
DATA_PROTOCOL = "data:".freeze
|
|
14
11
|
BLOB_PROTOCOL = "blob:".freeze
|
|
15
12
|
SELF = "'self'".freeze
|
|
@@ -158,13 +155,13 @@ module SecureHeaders
|
|
|
158
155
|
PLUGIN_TYPES => :source_list,
|
|
159
156
|
REFLECTED_XSS => :string,
|
|
160
157
|
REPORT_URI => :source_list,
|
|
161
|
-
SANDBOX => :
|
|
158
|
+
SANDBOX => :source_list,
|
|
162
159
|
SCRIPT_SRC => :source_list,
|
|
163
160
|
STYLE_SRC => :source_list,
|
|
164
161
|
UPGRADE_INSECURE_REQUESTS => :boolean
|
|
165
162
|
}.freeze
|
|
166
163
|
|
|
167
|
-
|
|
164
|
+
|
|
168
165
|
STAR_REGEXP = Regexp.new(Regexp.escape(STAR))
|
|
169
166
|
HTTP_SCHEME_REGEX = %r{\Ahttps?://}
|
|
170
167
|
|
|
@@ -181,6 +178,11 @@ module SecureHeaders
|
|
|
181
178
|
:preserve_schemes
|
|
182
179
|
].freeze
|
|
183
180
|
|
|
181
|
+
NONCES = [
|
|
182
|
+
:script_nonce,
|
|
183
|
+
:style_nonce
|
|
184
|
+
].freeze
|
|
185
|
+
|
|
184
186
|
module ClassMethods
|
|
185
187
|
# Public: generate a header name, value array that is user-agent-aware.
|
|
186
188
|
#
|
|
@@ -196,9 +198,11 @@ module SecureHeaders
|
|
|
196
198
|
# Does not validate the invididual values of the source expression (e.g.
|
|
197
199
|
# script_src => h*t*t*p: will not raise an exception)
|
|
198
200
|
def validate_config!(config)
|
|
199
|
-
return if config.nil? || config
|
|
200
|
-
raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config
|
|
201
|
-
|
|
201
|
+
return if config.nil? || config.opt_out?
|
|
202
|
+
raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config.directive_value(:default_src)
|
|
203
|
+
ContentSecurityPolicyConfig.attrs.each do |key|
|
|
204
|
+
value = config.directive_value(key)
|
|
205
|
+
next unless value
|
|
202
206
|
if META_CONFIGS.include?(key)
|
|
203
207
|
raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value") unless boolean?(value) || value.nil?
|
|
204
208
|
else
|
|
@@ -207,16 +211,13 @@ module SecureHeaders
|
|
|
207
211
|
end
|
|
208
212
|
end
|
|
209
213
|
|
|
210
|
-
# Public:
|
|
211
|
-
# actual value of the config.
|
|
214
|
+
# Public: check if a user agent supports CSP nonces
|
|
212
215
|
#
|
|
213
|
-
#
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
return false if config == OPT_OUT
|
|
219
|
-
config == combine_policies(config, additions)
|
|
216
|
+
# user_agent - a String or a UserAgent object
|
|
217
|
+
def nonces_supported?(user_agent)
|
|
218
|
+
user_agent = UserAgent.parse(user_agent) if user_agent.is_a?(String)
|
|
219
|
+
MODERN_BROWSERS.include?(user_agent.browser) ||
|
|
220
|
+
user_agent.browser == "Safari" && user_agent.version >= CSP::VERSION_10
|
|
220
221
|
end
|
|
221
222
|
|
|
222
223
|
# Public: combine the values from two different configs.
|
|
@@ -233,7 +234,7 @@ module SecureHeaders
|
|
|
233
234
|
# 3. if a value in additions does exist in the original config, the two
|
|
234
235
|
# values are joined.
|
|
235
236
|
def combine_policies(original, additions)
|
|
236
|
-
if original ==
|
|
237
|
+
if original == {}
|
|
237
238
|
raise ContentSecurityPolicyConfigError.new("Attempted to override an opt-out CSP config.")
|
|
238
239
|
end
|
|
239
240
|
|
|
@@ -34,6 +34,14 @@ module SecureHeaders
|
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
def content_security_policy_script_nonce
|
|
38
|
+
content_security_policy_nonce(:script)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def content_security_policy_style_nonce
|
|
42
|
+
content_security_policy_nonce(:style)
|
|
43
|
+
end
|
|
44
|
+
|
|
37
45
|
##
|
|
38
46
|
# Checks to see if the hashed code is expected and adds the hash source
|
|
39
47
|
# value to the current CSP.
|
data/lib/secure_headers.rb
CHANGED
|
@@ -14,18 +14,44 @@ require "secure_headers/middleware"
|
|
|
14
14
|
require "secure_headers/railtie"
|
|
15
15
|
require "secure_headers/view_helper"
|
|
16
16
|
require "useragent"
|
|
17
|
+
require "singleton"
|
|
17
18
|
|
|
18
19
|
# All headers (except for hpkp) have a default value. Provide SecureHeaders::OPT_OUT
|
|
19
20
|
# or ":optout_of_protection" as a config value to disable a given header
|
|
20
21
|
module SecureHeaders
|
|
21
|
-
|
|
22
|
+
class NoOpHeaderConfig
|
|
23
|
+
include Singleton
|
|
24
|
+
|
|
25
|
+
def boom(arg = nil)
|
|
26
|
+
raise "Illegal State: attempted to modify NoOpHeaderConfig. Create a new config instead."
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_h
|
|
30
|
+
{}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def dup
|
|
34
|
+
self.class.instance
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def opt_out?
|
|
38
|
+
true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
alias_method :[], :boom
|
|
42
|
+
alias_method :[]=, :boom
|
|
43
|
+
alias_method :keys, :boom
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
OPT_OUT = NoOpHeaderConfig.instance
|
|
22
47
|
SECURE_HEADERS_CONFIG = "secure_headers_request_config".freeze
|
|
23
48
|
NONCE_KEY = "secure_headers_content_security_policy_nonce".freeze
|
|
24
49
|
HTTPS = "https".freeze
|
|
25
50
|
CSP = ContentSecurityPolicy
|
|
26
51
|
|
|
27
52
|
ALL_HEADER_CLASSES = [
|
|
28
|
-
|
|
53
|
+
ContentSecurityPolicyConfig,
|
|
54
|
+
ContentSecurityPolicyReportOnlyConfig,
|
|
29
55
|
StrictTransportSecurity,
|
|
30
56
|
PublicKeyPins,
|
|
31
57
|
ReferrerPolicy,
|
|
@@ -36,7 +62,10 @@ module SecureHeaders
|
|
|
36
62
|
XXssProtection
|
|
37
63
|
].freeze
|
|
38
64
|
|
|
39
|
-
ALL_HEADERS_BESIDES_CSP = (
|
|
65
|
+
ALL_HEADERS_BESIDES_CSP = (
|
|
66
|
+
ALL_HEADER_CLASSES -
|
|
67
|
+
[ContentSecurityPolicyConfig, ContentSecurityPolicyReportOnlyConfig]
|
|
68
|
+
).freeze
|
|
40
69
|
|
|
41
70
|
# Headers set on http requests (excludes STS and HPKP)
|
|
42
71
|
HTTP_HEADER_CLASSES =
|
|
@@ -50,13 +79,25 @@ module SecureHeaders
|
|
|
50
79
|
#
|
|
51
80
|
# additions - a hash containing directives. e.g.
|
|
52
81
|
# script_src: %w(another-host.com)
|
|
53
|
-
def override_content_security_policy_directives(request, additions)
|
|
54
|
-
config =
|
|
55
|
-
|
|
56
|
-
|
|
82
|
+
def override_content_security_policy_directives(request, additions, target = nil)
|
|
83
|
+
config, target = config_and_target(request, target)
|
|
84
|
+
|
|
85
|
+
if [:both, :enforced].include?(target)
|
|
86
|
+
if config.csp.opt_out?
|
|
87
|
+
config.csp = ContentSecurityPolicyConfig.new({})
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
config.csp.merge!(additions)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
if [:both, :report_only].include?(target)
|
|
94
|
+
if config.csp_report_only.opt_out?
|
|
95
|
+
config.csp_report_only = ContentSecurityPolicyReportOnlyConfig.new({})
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
config.csp_report_only.merge!(additions)
|
|
57
99
|
end
|
|
58
100
|
|
|
59
|
-
config.dynamic_csp = config.current_csp.merge(additions)
|
|
60
101
|
override_secure_headers_request_config(request, config)
|
|
61
102
|
end
|
|
62
103
|
|
|
@@ -66,9 +107,17 @@ module SecureHeaders
|
|
|
66
107
|
#
|
|
67
108
|
# additions - a hash containing directives. e.g.
|
|
68
109
|
# script_src: %w(another-host.com)
|
|
69
|
-
def append_content_security_policy_directives(request, additions)
|
|
70
|
-
config =
|
|
71
|
-
|
|
110
|
+
def append_content_security_policy_directives(request, additions, target = nil)
|
|
111
|
+
config, target = config_and_target(request, target)
|
|
112
|
+
|
|
113
|
+
if [:both, :enforced].include?(target) && !config.csp.opt_out?
|
|
114
|
+
config.csp.append(additions)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
if [:both, :report_only].include?(target) && !config.csp_report_only.opt_out?
|
|
118
|
+
config.csp_report_only.append(additions)
|
|
119
|
+
end
|
|
120
|
+
|
|
72
121
|
override_secure_headers_request_config(request, config)
|
|
73
122
|
end
|
|
74
123
|
|
|
@@ -112,12 +161,28 @@ module SecureHeaders
|
|
|
112
161
|
# returned is meant to be merged into the header value from `@app.call(env)`
|
|
113
162
|
# in Rack middleware.
|
|
114
163
|
def header_hash_for(request)
|
|
115
|
-
config = config_for(request)
|
|
116
|
-
|
|
117
|
-
|
|
164
|
+
config = config_for(request, prevent_dup = true)
|
|
165
|
+
headers = config.cached_headers
|
|
166
|
+
user_agent = UserAgent.parse(request.user_agent)
|
|
167
|
+
|
|
168
|
+
if !config.csp.opt_out? && config.csp.modified?
|
|
169
|
+
headers = update_cached_csp(config.csp, headers, user_agent)
|
|
118
170
|
end
|
|
119
171
|
|
|
120
|
-
|
|
172
|
+
if !config.csp_report_only.opt_out? && config.csp_report_only.modified?
|
|
173
|
+
headers = update_cached_csp(config.csp_report_only, headers, user_agent)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
header_classes_for(request).each_with_object({}) do |klass, hash|
|
|
177
|
+
if header = headers[klass::CONFIG_KEY]
|
|
178
|
+
header_name, value = if [ContentSecurityPolicyConfig, ContentSecurityPolicyReportOnlyConfig].include?(klass)
|
|
179
|
+
csp_header_for_ua(header, user_agent)
|
|
180
|
+
else
|
|
181
|
+
header
|
|
182
|
+
end
|
|
183
|
+
hash[header_name] = value
|
|
184
|
+
end
|
|
185
|
+
end
|
|
121
186
|
end
|
|
122
187
|
|
|
123
188
|
# Public: specify which named override will be used for this request.
|
|
@@ -138,7 +203,7 @@ module SecureHeaders
|
|
|
138
203
|
#
|
|
139
204
|
# Returns the nonce
|
|
140
205
|
def content_security_policy_script_nonce(request)
|
|
141
|
-
content_security_policy_nonce(request,
|
|
206
|
+
content_security_policy_nonce(request, ContentSecurityPolicy::SCRIPT_SRC)
|
|
142
207
|
end
|
|
143
208
|
|
|
144
209
|
# Public: gets or creates a nonce for CSP.
|
|
@@ -147,7 +212,7 @@ module SecureHeaders
|
|
|
147
212
|
#
|
|
148
213
|
# Returns the nonce
|
|
149
214
|
def content_security_policy_style_nonce(request)
|
|
150
|
-
content_security_policy_nonce(request,
|
|
215
|
+
content_security_policy_nonce(request, ContentSecurityPolicy::STYLE_SRC)
|
|
151
216
|
end
|
|
152
217
|
|
|
153
218
|
# Public: Retreives the config for a given header type:
|
|
@@ -155,11 +220,15 @@ module SecureHeaders
|
|
|
155
220
|
# Checks to see if there is an override for this request, then
|
|
156
221
|
# Checks to see if a named override is used for this request, then
|
|
157
222
|
# Falls back to the global config
|
|
158
|
-
def config_for(request)
|
|
223
|
+
def config_for(request, prevent_dup = false)
|
|
159
224
|
config = request.env[SECURE_HEADERS_CONFIG] ||
|
|
160
225
|
Configuration.get(Configuration::DEFAULT_CONFIG)
|
|
161
226
|
|
|
162
|
-
|
|
227
|
+
|
|
228
|
+
# Global configs are frozen, per-request configs are not. When we're not
|
|
229
|
+
# making modifications to the config, prevent_dup ensures we don't dup
|
|
230
|
+
# the object unnecessarily. It's not necessarily frozen to begin with.
|
|
231
|
+
if config.frozen? && !prevent_dup
|
|
163
232
|
config.dup
|
|
164
233
|
else
|
|
165
234
|
config
|
|
@@ -167,13 +236,38 @@ module SecureHeaders
|
|
|
167
236
|
end
|
|
168
237
|
|
|
169
238
|
private
|
|
239
|
+
TARGETS = [:both, :enforced, :report_only]
|
|
240
|
+
def raise_on_unknown_target(target)
|
|
241
|
+
unless TARGETS.include?(target)
|
|
242
|
+
raise "Unrecognized target: #{target}. Must be [:both, :enforced, :report_only]"
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def config_and_target(request, target)
|
|
247
|
+
config = config_for(request)
|
|
248
|
+
target = guess_target(config) unless target
|
|
249
|
+
raise_on_unknown_target(target)
|
|
250
|
+
[config, target]
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def guess_target(config)
|
|
254
|
+
if !config.csp.opt_out? && !config.csp_report_only.opt_out?
|
|
255
|
+
:both
|
|
256
|
+
elsif !config.csp.opt_out?
|
|
257
|
+
:enforced
|
|
258
|
+
elsif !config.csp_report_only.opt_out?
|
|
259
|
+
:report_only
|
|
260
|
+
else
|
|
261
|
+
:both
|
|
262
|
+
end
|
|
263
|
+
end
|
|
170
264
|
|
|
171
265
|
# Private: gets or creates a nonce for CSP.
|
|
172
266
|
#
|
|
173
267
|
# Returns the nonce
|
|
174
268
|
def content_security_policy_nonce(request, script_or_style)
|
|
175
269
|
request.env[NONCE_KEY] ||= SecureRandom.base64(32).chomp
|
|
176
|
-
nonce_key = script_or_style ==
|
|
270
|
+
nonce_key = script_or_style == ContentSecurityPolicy::SCRIPT_SRC ? :script_nonce : :style_nonce
|
|
177
271
|
append_content_security_policy_directives(request, nonce_key => request.env[NONCE_KEY])
|
|
178
272
|
request.env[NONCE_KEY]
|
|
179
273
|
end
|
|
@@ -198,21 +292,12 @@ module SecureHeaders
|
|
|
198
292
|
end
|
|
199
293
|
end
|
|
200
294
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if header = headers[klass::CONFIG_KEY]
|
|
208
|
-
header_name, value = if klass == CSP
|
|
209
|
-
csp_header_for_ua(header, request)
|
|
210
|
-
else
|
|
211
|
-
header
|
|
212
|
-
end
|
|
213
|
-
hash[header_name] = value
|
|
214
|
-
end
|
|
215
|
-
end
|
|
295
|
+
def update_cached_csp(config, headers, user_agent)
|
|
296
|
+
headers = Configuration.send(:deep_copy, headers)
|
|
297
|
+
headers[config.class::CONFIG_KEY] = {}
|
|
298
|
+
variation = ContentSecurityPolicy.ua_to_variation(user_agent)
|
|
299
|
+
headers[config.class::CONFIG_KEY][variation] = ContentSecurityPolicy.make_header(config, user_agent)
|
|
300
|
+
headers
|
|
216
301
|
end
|
|
217
302
|
|
|
218
303
|
# Private: chooses the applicable CSP header for the provided user agent.
|
|
@@ -220,26 +305,8 @@ module SecureHeaders
|
|
|
220
305
|
# headers - a hash of header_config_key => [header_name, header_value]
|
|
221
306
|
#
|
|
222
307
|
# Returns a CSP [header, value] array
|
|
223
|
-
def csp_header_for_ua(headers,
|
|
224
|
-
headers[
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
# Private: optionally build a header with a given configure
|
|
228
|
-
#
|
|
229
|
-
# klass - corresponding Class for a given header
|
|
230
|
-
# config - A string, symbol, or hash config for the header
|
|
231
|
-
# user_agent - A string representing the UA (only used for CSP feature sniffing)
|
|
232
|
-
#
|
|
233
|
-
# Returns a 2 element array [header_name, header_value] or nil if config
|
|
234
|
-
# is OPT_OUT
|
|
235
|
-
def make_header(klass, header_config, user_agent = nil)
|
|
236
|
-
unless header_config == OPT_OUT
|
|
237
|
-
if klass == CSP
|
|
238
|
-
klass.make_header(header_config, user_agent)
|
|
239
|
-
else
|
|
240
|
-
klass.make_header(header_config)
|
|
241
|
-
end
|
|
242
|
-
end
|
|
308
|
+
def csp_header_for_ua(headers, user_agent)
|
|
309
|
+
headers[ContentSecurityPolicy.ua_to_variation(user_agent)]
|
|
243
310
|
end
|
|
244
311
|
end
|
|
245
312
|
|
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.5.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
|
|
@@ -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
|
|
|
@@ -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
|
|
@@ -51,7 +51,7 @@ module SecureHeaders
|
|
|
51
51
|
SecureHeaders.use_secure_headers_override(request, "my_custom_config")
|
|
52
52
|
expect(request.env[SECURE_HEADERS_CONFIG]).to be(Configuration.get("my_custom_config"))
|
|
53
53
|
_, env = middleware.call request.env
|
|
54
|
-
expect(env[
|
|
54
|
+
expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match("example.org")
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
context "secure_cookies" do
|
|
@@ -105,6 +105,18 @@ module SecureHeaders
|
|
|
105
105
|
_, env = cookie_middleware.call request.env
|
|
106
106
|
expect(env['Set-Cookie']).to eq("foo=bar")
|
|
107
107
|
end
|
|
108
|
+
|
|
109
|
+
it "sets the secure cookie flag correctly on interleaved http/https requests" do
|
|
110
|
+
Configuration.default { |config| config.cookies = { secure: true } }
|
|
111
|
+
|
|
112
|
+
request = Rack::Request.new("HTTPS" => "off")
|
|
113
|
+
_, env = cookie_middleware.call request.env
|
|
114
|
+
expect(env['Set-Cookie']).to eq("foo=bar")
|
|
115
|
+
|
|
116
|
+
request = Rack::Request.new("HTTPS" => "on")
|
|
117
|
+
_, env = cookie_middleware.call request.env
|
|
118
|
+
expect(env['Set-Cookie']).to eq("foo=bar; secure")
|
|
119
|
+
end
|
|
108
120
|
end
|
|
109
121
|
end
|
|
110
122
|
end
|
|
@@ -27,6 +27,16 @@ class Message < ERB
|
|
|
27
27
|
background-color: black;
|
|
28
28
|
}
|
|
29
29
|
<% end %>
|
|
30
|
+
|
|
31
|
+
<script nonce="<%= content_security_policy_script_nonce %>">
|
|
32
|
+
alert(1)
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<style nonce="<%= content_security_policy_style_nonce %>">
|
|
36
|
+
body {
|
|
37
|
+
background-color: black;
|
|
38
|
+
}
|
|
39
|
+
</style>
|
|
30
40
|
<%= @name %>
|
|
31
41
|
|
|
32
42
|
TEMPLATE
|
|
@@ -72,8 +82,11 @@ module SecureHeaders
|
|
|
72
82
|
|
|
73
83
|
before(:all) do
|
|
74
84
|
Configuration.default do |config|
|
|
75
|
-
config.csp
|
|
76
|
-
|
|
85
|
+
config.csp = {
|
|
86
|
+
:default_src => %w('self'),
|
|
87
|
+
:script_src => %w('self'),
|
|
88
|
+
:style_src => %w('self')
|
|
89
|
+
}
|
|
77
90
|
end
|
|
78
91
|
end
|
|
79
92
|
|
|
@@ -115,10 +128,10 @@ module SecureHeaders
|
|
|
115
128
|
Message.new(request).result
|
|
116
129
|
_, env = middleware.call request.env
|
|
117
130
|
|
|
118
|
-
expect(env[
|
|
119
|
-
expect(env[
|
|
120
|
-
expect(env[
|
|
121
|
-
expect(env[
|
|
131
|
+
expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/script-src[^;]*'#{Regexp.escape(expected_hash)}'/)
|
|
132
|
+
expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/script-src[^;]*'nonce-abc123'/)
|
|
133
|
+
expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/style-src[^;]*'nonce-abc123'/)
|
|
134
|
+
expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/style-src[^;]*'#{Regexp.escape(expected_style_hash)}'/)
|
|
122
135
|
end
|
|
123
136
|
end
|
|
124
137
|
end
|