secure_headers 3.4.0 → 4.0.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/.rspec +2 -0
- data/.rubocop.yml +3 -0
- data/.ruby-version +1 -1
- data/.travis.yml +8 -4
- data/CHANGELOG.md +113 -1
- data/CODE_OF_CONDUCT.md +46 -0
- data/CONTRIBUTING.md +41 -0
- data/Gemfile +8 -4
- data/Guardfile +1 -0
- data/LICENSE +4 -199
- data/README.md +63 -333
- data/Rakefile +22 -18
- data/docs/HPKP.md +17 -0
- data/docs/cookies.md +64 -0
- data/docs/hashes.md +64 -0
- data/docs/named_overrides_and_appends.md +107 -0
- data/docs/per_action_configuration.md +133 -0
- data/docs/sinatra.md +25 -0
- data/lib/secure_headers/configuration.rb +91 -44
- data/lib/secure_headers/hash_helper.rb +2 -1
- data/lib/secure_headers/headers/clear_site_data.rb +55 -0
- data/lib/secure_headers/headers/content_security_policy.rb +110 -51
- data/lib/secure_headers/headers/content_security_policy_config.rb +162 -0
- data/lib/secure_headers/headers/cookie.rb +21 -3
- data/lib/secure_headers/headers/expect_certificate_transparency.rb +70 -0
- data/lib/secure_headers/headers/policy_management.rb +143 -69
- data/lib/secure_headers/headers/public_key_pins.rb +5 -4
- data/lib/secure_headers/headers/referrer_policy.rb +5 -1
- data/lib/secure_headers/headers/strict_transport_security.rb +2 -1
- data/lib/secure_headers/headers/x_content_type_options.rb +2 -1
- data/lib/secure_headers/headers/x_download_options.rb +3 -2
- data/lib/secure_headers/headers/x_frame_options.rb +2 -1
- data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +3 -2
- data/lib/secure_headers/headers/x_xss_protection.rb +2 -1
- data/lib/secure_headers/middleware.rb +10 -9
- data/lib/secure_headers/railtie.rb +7 -6
- data/lib/secure_headers/utils/cookies_config.rb +13 -12
- data/lib/secure_headers/view_helper.rb +12 -3
- data/lib/secure_headers.rb +137 -55
- data/lib/tasks/tasks.rake +2 -1
- data/secure_headers.gemspec +13 -3
- data/spec/lib/secure_headers/configuration_spec.rb +13 -12
- data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +87 -0
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +59 -26
- data/spec/lib/secure_headers/headers/cookie_spec.rb +38 -20
- data/spec/lib/secure_headers/headers/expect_certificate_spec.rb +42 -0
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +78 -40
- data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +7 -6
- data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +22 -3
- data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +5 -4
- data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +2 -1
- data/spec/lib/secure_headers/headers/x_download_options_spec.rb +3 -2
- data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +2 -1
- data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +4 -3
- data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +4 -3
- data/spec/lib/secure_headers/middleware_spec.rb +39 -19
- data/spec/lib/secure_headers/view_helpers_spec.rb +21 -8
- data/spec/lib/secure_headers_spec.rb +304 -56
- data/spec/spec_helper.rb +13 -24
- data/upgrading-to-4-0.md +55 -0
- metadata +29 -7
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
module SecureHeaders
|
|
2
3
|
module ViewHelpers
|
|
3
4
|
include SecureHeaders::HashHelper
|
|
@@ -34,6 +35,14 @@ module SecureHeaders
|
|
|
34
35
|
end
|
|
35
36
|
end
|
|
36
37
|
|
|
38
|
+
def content_security_policy_script_nonce
|
|
39
|
+
content_security_policy_nonce(:script)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def content_security_policy_style_nonce
|
|
43
|
+
content_security_policy_nonce(:style)
|
|
44
|
+
end
|
|
45
|
+
|
|
37
46
|
##
|
|
38
47
|
# Checks to see if the hashed code is expected and adds the hash source
|
|
39
48
|
# value to the current CSP.
|
|
@@ -67,7 +76,7 @@ module SecureHeaders
|
|
|
67
76
|
end
|
|
68
77
|
|
|
69
78
|
content = capture(&block)
|
|
70
|
-
file_path = File.join(
|
|
79
|
+
file_path = File.join("app", "views", self.instance_variable_get(:@virtual_path) + ".html.erb")
|
|
71
80
|
|
|
72
81
|
if raise_error_on_unrecognized_hash
|
|
73
82
|
hash_value = hash_source(content)
|
|
@@ -87,9 +96,9 @@ module SecureHeaders
|
|
|
87
96
|
<<-EOF
|
|
88
97
|
\n\n*** WARNING: Unrecognized hash in #{file_path}!!! Value: #{hash_value} ***
|
|
89
98
|
#{content}
|
|
90
|
-
*** Run #{SECURE_HEADERS_RAKE_TASK} or add the following to config/
|
|
99
|
+
*** Run #{SECURE_HEADERS_RAKE_TASK} or add the following to config/secure_headers_generated_hashes.yml:***
|
|
91
100
|
#{file_path}:
|
|
92
|
-
- #{hash_value}\n\n
|
|
101
|
+
- \"#{hash_value}\"\n\n
|
|
93
102
|
NOTE: dynamic javascript is not supported using script hash integration
|
|
94
103
|
on purpose. It defeats the point of using it in the first place.
|
|
95
104
|
EOF
|
data/lib/secure_headers.rb
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
require "secure_headers/configuration"
|
|
2
3
|
require "secure_headers/hash_helper"
|
|
3
4
|
require "secure_headers/headers/cookie"
|
|
@@ -10,22 +11,52 @@ require "secure_headers/headers/x_content_type_options"
|
|
|
10
11
|
require "secure_headers/headers/x_download_options"
|
|
11
12
|
require "secure_headers/headers/x_permitted_cross_domain_policies"
|
|
12
13
|
require "secure_headers/headers/referrer_policy"
|
|
14
|
+
require "secure_headers/headers/clear_site_data"
|
|
15
|
+
require "secure_headers/headers/expect_certificate_transparency"
|
|
13
16
|
require "secure_headers/middleware"
|
|
14
17
|
require "secure_headers/railtie"
|
|
15
18
|
require "secure_headers/view_helper"
|
|
16
19
|
require "useragent"
|
|
20
|
+
require "singleton"
|
|
17
21
|
|
|
18
22
|
# All headers (except for hpkp) have a default value. Provide SecureHeaders::OPT_OUT
|
|
19
23
|
# or ":optout_of_protection" as a config value to disable a given header
|
|
20
24
|
module SecureHeaders
|
|
21
|
-
|
|
25
|
+
class NoOpHeaderConfig
|
|
26
|
+
include Singleton
|
|
27
|
+
|
|
28
|
+
def boom(*args)
|
|
29
|
+
raise "Illegal State: attempted to modify NoOpHeaderConfig. Create a new config instead."
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def to_h
|
|
33
|
+
{}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def dup
|
|
37
|
+
self.class.instance
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def opt_out?
|
|
41
|
+
true
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
alias_method :[], :boom
|
|
45
|
+
alias_method :[]=, :boom
|
|
46
|
+
alias_method :keys, :boom
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
OPT_OUT = NoOpHeaderConfig.instance
|
|
22
50
|
SECURE_HEADERS_CONFIG = "secure_headers_request_config".freeze
|
|
23
51
|
NONCE_KEY = "secure_headers_content_security_policy_nonce".freeze
|
|
24
52
|
HTTPS = "https".freeze
|
|
25
53
|
CSP = ContentSecurityPolicy
|
|
26
54
|
|
|
27
55
|
ALL_HEADER_CLASSES = [
|
|
28
|
-
|
|
56
|
+
ExpectCertificateTransparency,
|
|
57
|
+
ClearSiteData,
|
|
58
|
+
ContentSecurityPolicyConfig,
|
|
59
|
+
ContentSecurityPolicyReportOnlyConfig,
|
|
29
60
|
StrictTransportSecurity,
|
|
30
61
|
PublicKeyPins,
|
|
31
62
|
ReferrerPolicy,
|
|
@@ -36,7 +67,10 @@ module SecureHeaders
|
|
|
36
67
|
XXssProtection
|
|
37
68
|
].freeze
|
|
38
69
|
|
|
39
|
-
ALL_HEADERS_BESIDES_CSP = (
|
|
70
|
+
ALL_HEADERS_BESIDES_CSP = (
|
|
71
|
+
ALL_HEADER_CLASSES -
|
|
72
|
+
[ContentSecurityPolicyConfig, ContentSecurityPolicyReportOnlyConfig]
|
|
73
|
+
).freeze
|
|
40
74
|
|
|
41
75
|
# Headers set on http requests (excludes STS and HPKP)
|
|
42
76
|
HTTP_HEADER_CLASSES =
|
|
@@ -50,13 +84,25 @@ module SecureHeaders
|
|
|
50
84
|
#
|
|
51
85
|
# additions - a hash containing directives. e.g.
|
|
52
86
|
# script_src: %w(another-host.com)
|
|
53
|
-
def override_content_security_policy_directives(request, additions)
|
|
54
|
-
config =
|
|
55
|
-
|
|
56
|
-
|
|
87
|
+
def override_content_security_policy_directives(request, additions, target = nil)
|
|
88
|
+
config, target = config_and_target(request, target)
|
|
89
|
+
|
|
90
|
+
if [:both, :enforced].include?(target)
|
|
91
|
+
if config.csp.opt_out?
|
|
92
|
+
config.csp = ContentSecurityPolicyConfig.new({})
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
config.csp.merge!(additions)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
if [:both, :report_only].include?(target)
|
|
99
|
+
if config.csp_report_only.opt_out?
|
|
100
|
+
config.csp_report_only = ContentSecurityPolicyReportOnlyConfig.new({})
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
config.csp_report_only.merge!(additions)
|
|
57
104
|
end
|
|
58
105
|
|
|
59
|
-
config.dynamic_csp = config.current_csp.merge(additions)
|
|
60
106
|
override_secure_headers_request_config(request, config)
|
|
61
107
|
end
|
|
62
108
|
|
|
@@ -66,12 +112,25 @@ module SecureHeaders
|
|
|
66
112
|
#
|
|
67
113
|
# additions - a hash containing directives. e.g.
|
|
68
114
|
# script_src: %w(another-host.com)
|
|
69
|
-
def append_content_security_policy_directives(request, additions)
|
|
70
|
-
config =
|
|
71
|
-
|
|
115
|
+
def append_content_security_policy_directives(request, additions, target = nil)
|
|
116
|
+
config, target = config_and_target(request, target)
|
|
117
|
+
|
|
118
|
+
if [:both, :enforced].include?(target) && !config.csp.opt_out?
|
|
119
|
+
config.csp.append(additions)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
if [:both, :report_only].include?(target) && !config.csp_report_only.opt_out?
|
|
123
|
+
config.csp_report_only.append(additions)
|
|
124
|
+
end
|
|
125
|
+
|
|
72
126
|
override_secure_headers_request_config(request, config)
|
|
73
127
|
end
|
|
74
128
|
|
|
129
|
+
def use_content_security_policy_named_append(request, name)
|
|
130
|
+
additions = SecureHeaders::Configuration.named_appends(name).call(request)
|
|
131
|
+
append_content_security_policy_directives(request, additions)
|
|
132
|
+
end
|
|
133
|
+
|
|
75
134
|
# Public: override X-Frame-Options settings for this request.
|
|
76
135
|
#
|
|
77
136
|
# value - deny, sameorigin, or allowall
|
|
@@ -107,12 +166,29 @@ module SecureHeaders
|
|
|
107
166
|
# returned is meant to be merged into the header value from `@app.call(env)`
|
|
108
167
|
# in Rack middleware.
|
|
109
168
|
def header_hash_for(request)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
169
|
+
prevent_dup = true
|
|
170
|
+
config = config_for(request, prevent_dup)
|
|
171
|
+
headers = config.cached_headers
|
|
172
|
+
user_agent = UserAgent.parse(request.user_agent)
|
|
173
|
+
|
|
174
|
+
if !config.csp.opt_out? && config.csp.modified?
|
|
175
|
+
headers = update_cached_csp(config.csp, headers, user_agent)
|
|
113
176
|
end
|
|
114
177
|
|
|
115
|
-
|
|
178
|
+
if !config.csp_report_only.opt_out? && config.csp_report_only.modified?
|
|
179
|
+
headers = update_cached_csp(config.csp_report_only, headers, user_agent)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
header_classes_for(request).each_with_object({}) do |klass, hash|
|
|
183
|
+
if header = headers[klass::CONFIG_KEY]
|
|
184
|
+
header_name, value = if klass == ContentSecurityPolicyConfig || klass == ContentSecurityPolicyReportOnlyConfig
|
|
185
|
+
csp_header_for_ua(header, user_agent)
|
|
186
|
+
else
|
|
187
|
+
header
|
|
188
|
+
end
|
|
189
|
+
hash[header_name] = value
|
|
190
|
+
end
|
|
191
|
+
end
|
|
116
192
|
end
|
|
117
193
|
|
|
118
194
|
# Public: specify which named override will be used for this request.
|
|
@@ -133,7 +209,7 @@ module SecureHeaders
|
|
|
133
209
|
#
|
|
134
210
|
# Returns the nonce
|
|
135
211
|
def content_security_policy_script_nonce(request)
|
|
136
|
-
content_security_policy_nonce(request,
|
|
212
|
+
content_security_policy_nonce(request, ContentSecurityPolicy::SCRIPT_SRC)
|
|
137
213
|
end
|
|
138
214
|
|
|
139
215
|
# Public: gets or creates a nonce for CSP.
|
|
@@ -142,7 +218,7 @@ module SecureHeaders
|
|
|
142
218
|
#
|
|
143
219
|
# Returns the nonce
|
|
144
220
|
def content_security_policy_style_nonce(request)
|
|
145
|
-
content_security_policy_nonce(request,
|
|
221
|
+
content_security_policy_nonce(request, ContentSecurityPolicy::STYLE_SRC)
|
|
146
222
|
end
|
|
147
223
|
|
|
148
224
|
# Public: Retreives the config for a given header type:
|
|
@@ -150,11 +226,15 @@ module SecureHeaders
|
|
|
150
226
|
# Checks to see if there is an override for this request, then
|
|
151
227
|
# Checks to see if a named override is used for this request, then
|
|
152
228
|
# Falls back to the global config
|
|
153
|
-
def config_for(request)
|
|
229
|
+
def config_for(request, prevent_dup = false)
|
|
154
230
|
config = request.env[SECURE_HEADERS_CONFIG] ||
|
|
155
231
|
Configuration.get(Configuration::DEFAULT_CONFIG)
|
|
156
232
|
|
|
157
|
-
|
|
233
|
+
|
|
234
|
+
# Global configs are frozen, per-request configs are not. When we're not
|
|
235
|
+
# making modifications to the config, prevent_dup ensures we don't dup
|
|
236
|
+
# the object unnecessarily. It's not necessarily frozen to begin with.
|
|
237
|
+
if config.frozen? && !prevent_dup
|
|
158
238
|
config.dup
|
|
159
239
|
else
|
|
160
240
|
config
|
|
@@ -162,13 +242,38 @@ module SecureHeaders
|
|
|
162
242
|
end
|
|
163
243
|
|
|
164
244
|
private
|
|
245
|
+
TARGETS = [:both, :enforced, :report_only]
|
|
246
|
+
def raise_on_unknown_target(target)
|
|
247
|
+
unless TARGETS.include?(target)
|
|
248
|
+
raise "Unrecognized target: #{target}. Must be [:both, :enforced, :report_only]"
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def config_and_target(request, target)
|
|
253
|
+
config = config_for(request)
|
|
254
|
+
target = guess_target(config) unless target
|
|
255
|
+
raise_on_unknown_target(target)
|
|
256
|
+
[config, target]
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def guess_target(config)
|
|
260
|
+
if !config.csp.opt_out? && !config.csp_report_only.opt_out?
|
|
261
|
+
:both
|
|
262
|
+
elsif !config.csp.opt_out?
|
|
263
|
+
:enforced
|
|
264
|
+
elsif !config.csp_report_only.opt_out?
|
|
265
|
+
:report_only
|
|
266
|
+
else
|
|
267
|
+
:both
|
|
268
|
+
end
|
|
269
|
+
end
|
|
165
270
|
|
|
166
271
|
# Private: gets or creates a nonce for CSP.
|
|
167
272
|
#
|
|
168
273
|
# Returns the nonce
|
|
169
274
|
def content_security_policy_nonce(request, script_or_style)
|
|
170
275
|
request.env[NONCE_KEY] ||= SecureRandom.base64(32).chomp
|
|
171
|
-
nonce_key = script_or_style ==
|
|
276
|
+
nonce_key = script_or_style == ContentSecurityPolicy::SCRIPT_SRC ? :script_nonce : :style_nonce
|
|
172
277
|
append_content_security_policy_directives(request, nonce_key => request.env[NONCE_KEY])
|
|
173
278
|
request.env[NONCE_KEY]
|
|
174
279
|
end
|
|
@@ -193,21 +298,12 @@ module SecureHeaders
|
|
|
193
298
|
end
|
|
194
299
|
end
|
|
195
300
|
|
|
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
|
|
301
|
+
def update_cached_csp(config, headers, user_agent)
|
|
302
|
+
headers = Configuration.send(:deep_copy, headers)
|
|
303
|
+
headers[config.class::CONFIG_KEY] = {}
|
|
304
|
+
variation = ContentSecurityPolicy.ua_to_variation(user_agent)
|
|
305
|
+
headers[config.class::CONFIG_KEY][variation] = ContentSecurityPolicy.make_header(config, user_agent)
|
|
306
|
+
headers
|
|
211
307
|
end
|
|
212
308
|
|
|
213
309
|
# Private: chooses the applicable CSP header for the provided user agent.
|
|
@@ -215,26 +311,8 @@ module SecureHeaders
|
|
|
215
311
|
# headers - a hash of header_config_key => [header_name, header_value]
|
|
216
312
|
#
|
|
217
313
|
# 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
|
|
314
|
+
def csp_header_for_ua(headers, user_agent)
|
|
315
|
+
headers[ContentSecurityPolicy.ua_to_variation(user_agent)]
|
|
238
316
|
end
|
|
239
317
|
end
|
|
240
318
|
|
|
@@ -267,4 +345,8 @@ module SecureHeaders
|
|
|
267
345
|
def override_x_frame_options(value)
|
|
268
346
|
SecureHeaders.override_x_frame_options(request, value)
|
|
269
347
|
end
|
|
348
|
+
|
|
349
|
+
def use_content_security_policy_named_append(name)
|
|
350
|
+
SecureHeaders.use_content_security_policy_named_append(request, name)
|
|
351
|
+
end
|
|
270
352
|
end
|
data/lib/tasks/tasks.rake
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
INLINE_SCRIPT_REGEX = /(<script(\s*(?!src)([\w\-])+=([\"\'])[^\"\']+\4)*\s*>)(.*?)<\/script>/mx unless defined? INLINE_SCRIPT_REGEX
|
|
2
3
|
INLINE_STYLE_REGEX = /(<style[^>]*>)(.*?)<\/style>/mx unless defined? INLINE_STYLE_REGEX
|
|
3
4
|
INLINE_HASH_SCRIPT_HELPER_REGEX = /<%=\s?hashed_javascript_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx unless defined? INLINE_HASH_SCRIPT_HELPER_REGEX
|
|
@@ -73,7 +74,7 @@ namespace :secure_headers do
|
|
|
73
74
|
end
|
|
74
75
|
end
|
|
75
76
|
|
|
76
|
-
File.open(SecureHeaders::Configuration::HASH_CONFIG_FILE,
|
|
77
|
+
File.open(SecureHeaders::Configuration::HASH_CONFIG_FILE, "w") do |file|
|
|
77
78
|
file.write(script_hashes.to_yaml)
|
|
78
79
|
end
|
|
79
80
|
|
data/secure_headers.gemspec
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
|
2
|
+
# frozen_string_literal: true
|
|
2
3
|
Gem::Specification.new do |gem|
|
|
3
4
|
gem.name = "secure_headers"
|
|
4
|
-
gem.version = "
|
|
5
|
+
gem.version = "4.0.0"
|
|
5
6
|
gem.authors = ["Neil Matatall"]
|
|
6
7
|
gem.email = ["neil.matatall@gmail.com"]
|
|
7
|
-
gem.description =
|
|
8
|
+
gem.description = "Manages application of security headers with many safe defaults."
|
|
8
9
|
gem.summary = 'Add easily configured security headers to responses
|
|
9
10
|
including content-security-policy, x-frame-options,
|
|
10
11
|
strict-transport-security, etc.'
|
|
@@ -15,5 +16,14 @@ Gem::Specification.new do |gem|
|
|
|
15
16
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
|
16
17
|
gem.require_paths = ["lib"]
|
|
17
18
|
gem.add_development_dependency "rake"
|
|
18
|
-
gem.add_dependency "useragent"
|
|
19
|
+
gem.add_dependency "useragent", ">= 0.15.0"
|
|
20
|
+
|
|
21
|
+
# TODO: delete this after 4.1 is cut or a number of 4.0.x releases have occurred
|
|
22
|
+
gem.post_install_message = <<-POST_INSTALL
|
|
23
|
+
|
|
24
|
+
**********
|
|
25
|
+
:wave: secure_headers 4.0 introduces a lot of breaking changes (in the name of security!). It's highly likely you will need to update your secure_headers cookie configuration to avoid breaking things. See the upgrade guide for details: https://github.com/twitter/secureheaders/blob/master/upgrading-to-4-0.md
|
|
26
|
+
**********
|
|
27
|
+
|
|
28
|
+
POST_INSTALL
|
|
19
29
|
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "spec_helper"
|
|
2
3
|
|
|
3
4
|
module SecureHeaders
|
|
4
5
|
describe Configuration do
|
|
@@ -36,8 +37,8 @@ module SecureHeaders
|
|
|
36
37
|
|
|
37
38
|
config = Configuration.get(:test_override)
|
|
38
39
|
noop = Configuration.get(Configuration::NOOP_CONFIGURATION)
|
|
39
|
-
[:csp, :
|
|
40
|
-
expect(config.send(key)).to eq(noop.send(key))
|
|
40
|
+
[:csp, :csp_report_only, :cookies].each do |key|
|
|
41
|
+
expect(config.send(key)).to eq(noop.send(key))
|
|
41
42
|
end
|
|
42
43
|
end
|
|
43
44
|
|
|
@@ -65,12 +66,12 @@ module SecureHeaders
|
|
|
65
66
|
default = Configuration.get
|
|
66
67
|
override = Configuration.get(:override)
|
|
67
68
|
|
|
68
|
-
expect(override.csp).not_to
|
|
69
|
+
expect(override.csp.directive_value(:default_src)).not_to be(default.csp.directive_value(:default_src))
|
|
69
70
|
end
|
|
70
71
|
|
|
71
72
|
it "allows you to override an override" do
|
|
72
73
|
Configuration.override(:override) do |config|
|
|
73
|
-
config.csp = { default_src: %w('self')}
|
|
74
|
+
config.csp = { default_src: %w('self'), script_src: %w('self')}
|
|
74
75
|
end
|
|
75
76
|
|
|
76
77
|
Configuration.override(:second_override, :override) do |config|
|
|
@@ -78,17 +79,17 @@ module SecureHeaders
|
|
|
78
79
|
end
|
|
79
80
|
|
|
80
81
|
original_override = Configuration.get(:override)
|
|
81
|
-
expect(original_override.csp).to eq(default_src: %w('self'))
|
|
82
|
+
expect(original_override.csp.to_h).to eq(default_src: %w('self'), script_src: %w('self'))
|
|
82
83
|
override_config = Configuration.get(:second_override)
|
|
83
|
-
expect(override_config.csp).to eq(default_src: %w('self'), script_src: %w(example.org))
|
|
84
|
+
expect(override_config.csp.to_h).to eq(default_src: %w('self'), script_src: %w('self' example.org))
|
|
84
85
|
end
|
|
85
86
|
|
|
86
87
|
it "deprecates the secure_cookies configuration" do
|
|
87
|
-
expect
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
expect {
|
|
89
|
+
Configuration.default do |config|
|
|
90
|
+
config.secure_cookies = true
|
|
91
|
+
end
|
|
92
|
+
}.to raise_error(ArgumentError)
|
|
92
93
|
end
|
|
93
94
|
end
|
|
94
95
|
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "spec_helper"
|
|
3
|
+
|
|
4
|
+
module SecureHeaders
|
|
5
|
+
describe ClearSiteData do
|
|
6
|
+
describe "make_header" do
|
|
7
|
+
it "returns nil with nil config" do
|
|
8
|
+
expect(described_class.make_header).to be_nil
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it "returns nil with empty config" do
|
|
12
|
+
expect(described_class.make_header([])).to be_nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "returns nil with opt-out config" do
|
|
16
|
+
expect(described_class.make_header(OPT_OUT)).to be_nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "returns all types with `true` config" do
|
|
20
|
+
name, value = described_class.make_header(true)
|
|
21
|
+
|
|
22
|
+
expect(name).to eq(ClearSiteData::HEADER_NAME)
|
|
23
|
+
expect(value).to eq(
|
|
24
|
+
%("cache", "cookies", "storage", "executionContexts")
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "returns specified types" do
|
|
29
|
+
name, value = described_class.make_header(["foo", "bar"])
|
|
30
|
+
|
|
31
|
+
expect(name).to eq(ClearSiteData::HEADER_NAME)
|
|
32
|
+
expect(value).to eq(%("foo", "bar"))
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
describe "validate_config!" do
|
|
37
|
+
it "succeeds for `true` config" do
|
|
38
|
+
expect do
|
|
39
|
+
described_class.validate_config!(true)
|
|
40
|
+
end.not_to raise_error
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "succeeds for `nil` config" do
|
|
44
|
+
expect do
|
|
45
|
+
described_class.validate_config!(nil)
|
|
46
|
+
end.not_to raise_error
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "succeeds for opt-out config" do
|
|
50
|
+
expect do
|
|
51
|
+
described_class.validate_config!(OPT_OUT)
|
|
52
|
+
end.not_to raise_error
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "succeeds for empty config" do
|
|
56
|
+
expect do
|
|
57
|
+
described_class.validate_config!([])
|
|
58
|
+
end.not_to raise_error
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "succeeds for Array of Strings config" do
|
|
62
|
+
expect do
|
|
63
|
+
described_class.validate_config!(["foo"])
|
|
64
|
+
end.not_to raise_error
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "fails for Array of non-String config" do
|
|
68
|
+
expect do
|
|
69
|
+
described_class.validate_config!([1])
|
|
70
|
+
end.to raise_error(ClearSiteDataConfigError)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it "fails for other types of config" do
|
|
74
|
+
expect do
|
|
75
|
+
described_class.validate_config!(:cookies)
|
|
76
|
+
end.to raise_error(ClearSiteDataConfigError)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
describe "make_header_value" do
|
|
81
|
+
it "returns a string of quoted values that are comma separated" do
|
|
82
|
+
value = described_class.make_header_value(["foo", "bar"])
|
|
83
|
+
expect(value).to eq(%("foo", "bar"))
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|