secure_headers 3.7.0 → 3.8.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 +5 -5
- data/CHANGELOG.md +20 -0
- data/README.md +1 -0
- data/docs/cookies.md +3 -2
- data/lib/secure_headers/headers/content_security_policy.rb +48 -7
- data/lib/secure_headers/headers/content_security_policy_config.rb +1 -0
- data/lib/secure_headers/headers/cookie.rb +7 -1
- data/lib/secure_headers/headers/expect_certificate_transparency.rb +1 -1
- data/lib/secure_headers/headers/policy_management.rb +96 -47
- data/lib/secure_headers/utils/cookies_config.rb +6 -4
- data/secure_headers.gemspec +1 -1
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +35 -9
- data/spec/lib/secure_headers/headers/cookie_spec.rb +22 -25
- data/spec/lib/secure_headers/headers/{expect_certificate_spec.rb → expect_certificate_transparency_spec.rb} +3 -3
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +31 -0
- metadata +5 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d605beb08177e1325edd0726b0cb551d3ac71b6dc6da7c1261d6f30b60db7695
|
|
4
|
+
data.tar.gz: f166e3735db90264366d654520269fb3cfb696ce9747c1ce92c11927d3322cb2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 784ea802225dbe362fa66d56022f13a1e8d58f71cac325959a7c28c4cb0f0acc25a478f794ef1286d3e951abb06f5c2fd25a3e448170643db6946c97027a3fe6
|
|
7
|
+
data.tar.gz: 19abcb3f648b322ac8db0ca9352e53a758be5253d110e67224e8632366efde66a034f9777dc77946ef897dbc99e9dbf72edca743a1b184ca8b336c0a63296f7c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
## 3.8.0
|
|
2
|
+
|
|
3
|
+
Fixes semicolon injection issue reported by @mvgijssel see https://github.com/twitter/secure_headers/issues/418
|
|
4
|
+
|
|
5
|
+
## 3.7.4
|
|
6
|
+
|
|
7
|
+
Backport SameSite=None functionality into 3.x line
|
|
8
|
+
|
|
9
|
+
## 3.7.3
|
|
10
|
+
|
|
11
|
+
- Updates `Expect-CT` header to use a comma separator between directives, as specified in the most current spec.
|
|
12
|
+
|
|
13
|
+
## 3.7.2
|
|
14
|
+
|
|
15
|
+
- Adds support for `worker-src` CSP directive to 3.x line (https://github.com/twitter/secureheaders/pull/364)
|
|
16
|
+
|
|
17
|
+
## 3.7.1
|
|
18
|
+
|
|
19
|
+
Fix support for the sandbox attribute of CSP. `true` and `[]` represent the maximally restricted policy (`sandbox;`) and validate other values.
|
|
20
|
+
|
|
1
21
|
## 3.7.0
|
|
2
22
|
|
|
3
23
|
Adds support for the `Expect-CT` header (@jacobbednarz: https://github.com/twitter/secureheaders/pull/322)
|
data/README.md
CHANGED
|
@@ -103,6 +103,7 @@ SecureHeaders::Configuration.default do |config|
|
|
|
103
103
|
plugin_types: %w(application/x-shockwave-flash),
|
|
104
104
|
script_src: %w('self'),
|
|
105
105
|
style_src: %w('unsafe-inline'),
|
|
106
|
+
worker_src: %w('self'),
|
|
106
107
|
upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/
|
|
107
108
|
report_uri: %w(https://report-uri.io/example-csp)
|
|
108
109
|
}
|
data/docs/cookies.md
CHANGED
|
@@ -38,13 +38,14 @@ config.cookies = {
|
|
|
38
38
|
}
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
`Strict` and `
|
|
41
|
+
`Strict`, `Lax`, and `None` enforcement modes can also be specified using a Hash.
|
|
42
42
|
|
|
43
43
|
```ruby
|
|
44
44
|
config.cookies = {
|
|
45
45
|
samesite: {
|
|
46
46
|
strict: { only: ['_rails_session'] },
|
|
47
|
-
lax: { only: ['_guest'] }
|
|
47
|
+
lax: { only: ['_guest'] },
|
|
48
|
+
none: { only: ['_tracking'] },
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
```
|
|
@@ -80,20 +80,55 @@ module SecureHeaders
|
|
|
80
80
|
case DIRECTIVE_VALUE_TYPES[directive_name]
|
|
81
81
|
when :boolean
|
|
82
82
|
symbol_to_hyphen_case(directive_name) if @config.directive_value(directive_name)
|
|
83
|
-
when :
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
when :sandbox_list
|
|
84
|
+
build_sandbox_list_directive(directive_name)
|
|
85
|
+
when :media_type_list
|
|
86
|
+
build_media_type_list_directive(directive_name)
|
|
87
|
+
when :source_list
|
|
88
|
+
build_source_list_directive(directive_name)
|
|
87
89
|
end
|
|
88
90
|
end.compact.join("; ")
|
|
89
91
|
end
|
|
90
92
|
|
|
93
|
+
def build_sandbox_list_directive(directive)
|
|
94
|
+
return unless sandbox_list = @config.directive_value(directive)
|
|
95
|
+
max_strict_policy = case sandbox_list
|
|
96
|
+
when Array
|
|
97
|
+
sandbox_list.empty?
|
|
98
|
+
when true
|
|
99
|
+
true
|
|
100
|
+
else
|
|
101
|
+
false
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# A maximally strict sandbox policy is just the `sandbox` directive,
|
|
105
|
+
# whith no configuraiton values.
|
|
106
|
+
if max_strict_policy
|
|
107
|
+
symbol_to_hyphen_case(directive)
|
|
108
|
+
elsif sandbox_list && sandbox_list.any?
|
|
109
|
+
[
|
|
110
|
+
symbol_to_hyphen_case(directive),
|
|
111
|
+
sandbox_list.uniq
|
|
112
|
+
].join(" ")
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def build_media_type_list_directive(directive)
|
|
117
|
+
return unless media_type_list = @config.directive_value(directive)
|
|
118
|
+
if media_type_list && media_type_list.any?
|
|
119
|
+
[
|
|
120
|
+
symbol_to_hyphen_case(directive),
|
|
121
|
+
media_type_list.uniq
|
|
122
|
+
].join(" ")
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
91
126
|
# Private: builds a string that represents one directive in a minified form.
|
|
92
127
|
#
|
|
93
128
|
# directive_name - a symbol representing the various ALL_DIRECTIVES
|
|
94
129
|
#
|
|
95
130
|
# Returns a string representing a directive.
|
|
96
|
-
def
|
|
131
|
+
def build_source_list_directive(directive)
|
|
97
132
|
source_list = case directive
|
|
98
133
|
when :child_src
|
|
99
134
|
if supported_directives.include?(:child_src)
|
|
@@ -107,8 +142,14 @@ module SecureHeaders
|
|
|
107
142
|
@config.directive_value(directive)
|
|
108
143
|
end
|
|
109
144
|
return unless source_list && source_list.any?
|
|
110
|
-
normalized_source_list = minify_source_list(directive, source_list)
|
|
111
|
-
|
|
145
|
+
normalized_source_list = minify_source_list(directive, source_list).join(" ")
|
|
146
|
+
|
|
147
|
+
if normalized_source_list.include?(";")
|
|
148
|
+
Kernel.warn("#{directive} contains a ; in '#{normalized_source_list}' which will raise an error in future versions. It has been replaced with a blank space.")
|
|
149
|
+
end
|
|
150
|
+
escaped_source_list = normalized_source_list.gsub(";", " ")
|
|
151
|
+
|
|
152
|
+
[symbol_to_hyphen_case(directive), escaped_source_list].join(" ").strip
|
|
112
153
|
end
|
|
113
154
|
|
|
114
155
|
# If a directive contains *, all other values are omitted.
|
|
@@ -81,11 +81,13 @@ module SecureHeaders
|
|
|
81
81
|
"SameSite=Lax"
|
|
82
82
|
elsif flag_samesite_strict?
|
|
83
83
|
"SameSite=Strict"
|
|
84
|
+
elsif flag_samesite_none?
|
|
85
|
+
"SameSite=None"
|
|
84
86
|
end
|
|
85
87
|
end
|
|
86
88
|
|
|
87
89
|
def flag_samesite?
|
|
88
|
-
flag_samesite_lax? || flag_samesite_strict?
|
|
90
|
+
flag_samesite_lax? || flag_samesite_strict? || flag_samesite_none?
|
|
89
91
|
end
|
|
90
92
|
|
|
91
93
|
def flag_samesite_lax?
|
|
@@ -96,6 +98,10 @@ module SecureHeaders
|
|
|
96
98
|
flag_samesite_enforcement?(:strict)
|
|
97
99
|
end
|
|
98
100
|
|
|
101
|
+
def flag_samesite_none?
|
|
102
|
+
flag_samesite_enforcement?(:none)
|
|
103
|
+
end
|
|
104
|
+
|
|
99
105
|
def flag_samesite_enforcement?(mode)
|
|
100
106
|
return unless config[:samesite]
|
|
101
107
|
|
|
@@ -65,10 +65,13 @@ module SecureHeaders
|
|
|
65
65
|
BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content
|
|
66
66
|
MANIFEST_SRC = :manifest_src
|
|
67
67
|
UPGRADE_INSECURE_REQUESTS = :upgrade_insecure_requests
|
|
68
|
+
WORKER_SRC = :worker_src
|
|
69
|
+
|
|
68
70
|
DIRECTIVES_3_0 = [
|
|
69
71
|
DIRECTIVES_2_0,
|
|
70
72
|
BLOCK_ALL_MIXED_CONTENT,
|
|
71
73
|
MANIFEST_SRC,
|
|
74
|
+
WORKER_SRC,
|
|
72
75
|
UPGRADE_INSECURE_REQUESTS
|
|
73
76
|
].flatten.freeze
|
|
74
77
|
|
|
@@ -79,6 +82,7 @@ module SecureHeaders
|
|
|
79
82
|
FIREFOX_UNSUPPORTED_DIRECTIVES = [
|
|
80
83
|
BLOCK_ALL_MIXED_CONTENT,
|
|
81
84
|
CHILD_SRC,
|
|
85
|
+
WORKER_SRC,
|
|
82
86
|
PLUGIN_TYPES
|
|
83
87
|
].freeze
|
|
84
88
|
|
|
@@ -88,6 +92,7 @@ module SecureHeaders
|
|
|
88
92
|
|
|
89
93
|
FIREFOX_46_UNSUPPORTED_DIRECTIVES = [
|
|
90
94
|
BLOCK_ALL_MIXED_CONTENT,
|
|
95
|
+
WORKER_SRC,
|
|
91
96
|
PLUGIN_TYPES
|
|
92
97
|
].freeze
|
|
93
98
|
|
|
@@ -109,18 +114,6 @@ module SecureHeaders
|
|
|
109
114
|
# everything else is in between.
|
|
110
115
|
BODY_DIRECTIVES = ALL_DIRECTIVES - [DEFAULT_SRC, REPORT_URI]
|
|
111
116
|
|
|
112
|
-
# These are directives that do not inherit the default-src value. This is
|
|
113
|
-
# useful when calling #combine_policies.
|
|
114
|
-
NON_FETCH_SOURCES = [
|
|
115
|
-
BASE_URI,
|
|
116
|
-
FORM_ACTION,
|
|
117
|
-
FRAME_ANCESTORS,
|
|
118
|
-
PLUGIN_TYPES,
|
|
119
|
-
REPORT_URI
|
|
120
|
-
]
|
|
121
|
-
|
|
122
|
-
FETCH_SOURCES = ALL_DIRECTIVES - NON_FETCH_SOURCES
|
|
123
|
-
|
|
124
117
|
VARIATIONS = {
|
|
125
118
|
"Chrome" => CHROME_DIRECTIVES,
|
|
126
119
|
"Opera" => CHROME_DIRECTIVES,
|
|
@@ -148,14 +141,31 @@ module SecureHeaders
|
|
|
148
141
|
MANIFEST_SRC => :source_list,
|
|
149
142
|
MEDIA_SRC => :source_list,
|
|
150
143
|
OBJECT_SRC => :source_list,
|
|
151
|
-
PLUGIN_TYPES => :
|
|
144
|
+
PLUGIN_TYPES => :media_type_list,
|
|
152
145
|
REPORT_URI => :source_list,
|
|
153
|
-
SANDBOX => :
|
|
146
|
+
SANDBOX => :sandbox_list,
|
|
154
147
|
SCRIPT_SRC => :source_list,
|
|
155
148
|
STYLE_SRC => :source_list,
|
|
149
|
+
WORKER_SRC => :source_list,
|
|
156
150
|
UPGRADE_INSECURE_REQUESTS => :boolean
|
|
157
151
|
}.freeze
|
|
158
152
|
|
|
153
|
+
# These are directives that don't have use a source list, and hence do not
|
|
154
|
+
# inherit the default-src value.
|
|
155
|
+
NON_SOURCE_LIST_SOURCES = DIRECTIVE_VALUE_TYPES.select do |_, type|
|
|
156
|
+
type != :source_list
|
|
157
|
+
end.keys.freeze
|
|
158
|
+
|
|
159
|
+
# These are directives that take a source list, but that do not inherit
|
|
160
|
+
# the default-src value.
|
|
161
|
+
NON_FETCH_SOURCES = [
|
|
162
|
+
BASE_URI,
|
|
163
|
+
FORM_ACTION,
|
|
164
|
+
FRAME_ANCESTORS,
|
|
165
|
+
REPORT_URI
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
FETCH_SOURCES = ALL_DIRECTIVES - NON_FETCH_SOURCES - NON_SOURCE_LIST_SOURCES
|
|
159
169
|
|
|
160
170
|
STAR_REGEXP = Regexp.new(Regexp.escape(STAR))
|
|
161
171
|
HTTP_SCHEME_REGEX = %r{\Ahttps?://}
|
|
@@ -253,7 +263,7 @@ module SecureHeaders
|
|
|
253
263
|
# when each hash contains a value for a given key.
|
|
254
264
|
def merge_policy_additions(original, additions)
|
|
255
265
|
original.merge(additions) do |directive, lhs, rhs|
|
|
256
|
-
if
|
|
266
|
+
if list_directive?(directive)
|
|
257
267
|
(lhs.to_a + rhs.to_a).compact.uniq
|
|
258
268
|
else
|
|
259
269
|
rhs
|
|
@@ -261,20 +271,27 @@ module SecureHeaders
|
|
|
261
271
|
end.reject { |_, value| value.nil? || value == [] } # this mess prevents us from adding empty directives.
|
|
262
272
|
end
|
|
263
273
|
|
|
274
|
+
# Returns True if a directive expects a list of values and False otherwise.
|
|
275
|
+
def list_directive?(directive)
|
|
276
|
+
source_list?(directive) ||
|
|
277
|
+
sandbox_list?(directive) ||
|
|
278
|
+
media_type_list?(directive)
|
|
279
|
+
end
|
|
280
|
+
|
|
264
281
|
# For each directive in additions that does not exist in the original config,
|
|
265
282
|
# copy the default-src value to the original config. This modifies the original hash.
|
|
266
283
|
def populate_fetch_source_with_default!(original, additions)
|
|
267
284
|
# in case we would be appending to an empty directive, fill it with the default-src value
|
|
268
285
|
additions.each_key do |directive|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
286
|
+
directive = if directive.to_s.end_with?("_nonce")
|
|
287
|
+
directive.to_s.gsub(/_nonce/, "_src").to_sym
|
|
288
|
+
else
|
|
289
|
+
directive
|
|
290
|
+
end
|
|
291
|
+
# Don't set a default if directive has an existing value
|
|
292
|
+
next if original[directive]
|
|
293
|
+
if FETCH_SOURCES.include?(directive)
|
|
294
|
+
original[directive] = default_for(directive, original)
|
|
278
295
|
end
|
|
279
296
|
end
|
|
280
297
|
end
|
|
@@ -285,45 +302,77 @@ module SecureHeaders
|
|
|
285
302
|
original[DEFAULT_SRC]
|
|
286
303
|
end
|
|
287
304
|
|
|
288
|
-
def nonce_added?(original, additions)
|
|
289
|
-
[:script_nonce, :style_nonce].each do |nonce|
|
|
290
|
-
if additions[nonce] && !original[nonce]
|
|
291
|
-
return true
|
|
292
|
-
end
|
|
293
|
-
end
|
|
294
|
-
end
|
|
295
|
-
|
|
296
305
|
def source_list?(directive)
|
|
297
306
|
DIRECTIVE_VALUE_TYPES[directive] == :source_list
|
|
298
307
|
end
|
|
299
308
|
|
|
309
|
+
def sandbox_list?(directive)
|
|
310
|
+
DIRECTIVE_VALUE_TYPES[directive] == :sandbox_list
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def media_type_list?(directive)
|
|
314
|
+
DIRECTIVE_VALUE_TYPES[directive] == :media_type_list
|
|
315
|
+
end
|
|
316
|
+
|
|
300
317
|
# Private: Validates that the configuration has a valid type, or that it is a valid
|
|
301
318
|
# source expression.
|
|
302
|
-
def validate_directive!(directive,
|
|
319
|
+
def validate_directive!(directive, value)
|
|
320
|
+
ensure_valid_directive!(directive)
|
|
303
321
|
case ContentSecurityPolicy::DIRECTIVE_VALUE_TYPES[directive]
|
|
304
322
|
when :boolean
|
|
305
|
-
unless boolean?(
|
|
306
|
-
raise ContentSecurityPolicyConfigError.new("#{directive} must be a boolean value")
|
|
307
|
-
end
|
|
308
|
-
when :string
|
|
309
|
-
unless source_expression.is_a?(String)
|
|
310
|
-
raise ContentSecurityPolicyConfigError.new("#{directive} Must be a string. Found #{config.class}: #{config} value")
|
|
323
|
+
unless boolean?(value)
|
|
324
|
+
raise ContentSecurityPolicyConfigError.new("#{directive} must be a boolean. Found #{value.class} value")
|
|
311
325
|
end
|
|
326
|
+
when :sandbox_list
|
|
327
|
+
validate_sandbox_expression!(directive, value)
|
|
328
|
+
when :media_type_list
|
|
329
|
+
validate_media_type_expression!(directive, value)
|
|
330
|
+
when :source_list
|
|
331
|
+
validate_source_expression!(directive, value)
|
|
312
332
|
else
|
|
313
|
-
|
|
333
|
+
raise ContentSecurityPolicyConfigError.new("Unknown directive #{directive}")
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# Private: validates that a sandbox token expression:
|
|
338
|
+
# 1. is an array of strings or optionally `true` (to enable maximal sandboxing)
|
|
339
|
+
# 2. For arrays, each element is of the form allow-*
|
|
340
|
+
def validate_sandbox_expression!(directive, sandbox_token_expression)
|
|
341
|
+
# We support sandbox: true to indicate a maximally secure sandbox.
|
|
342
|
+
return if boolean?(sandbox_token_expression) && sandbox_token_expression == true
|
|
343
|
+
ensure_array_of_strings!(directive, sandbox_token_expression)
|
|
344
|
+
valid = sandbox_token_expression.compact.all? do |v|
|
|
345
|
+
v.is_a?(String) && v.start_with?("allow-")
|
|
346
|
+
end
|
|
347
|
+
if !valid
|
|
348
|
+
raise ContentSecurityPolicyConfigError.new("#{directive} must be True or an array of zero or more sandbox token strings (ex. allow-forms)")
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Private: validates that a media type expression:
|
|
353
|
+
# 1. is an array of strings
|
|
354
|
+
# 2. each element is of the form type/subtype
|
|
355
|
+
def validate_media_type_expression!(directive, media_type_expression)
|
|
356
|
+
ensure_array_of_strings!(directive, media_type_expression)
|
|
357
|
+
valid = media_type_expression.compact.all? do |v|
|
|
358
|
+
# All media types are of the form: <type from RFC 2045> "/" <subtype from RFC 2045>.
|
|
359
|
+
v =~ /\A.+\/.+\z/
|
|
360
|
+
end
|
|
361
|
+
if !valid
|
|
362
|
+
raise ContentSecurityPolicyConfigError.new("#{directive} must be an array of valid media types (ex. application/pdf)")
|
|
314
363
|
end
|
|
315
364
|
end
|
|
316
365
|
|
|
317
366
|
# Private: validates that a source expression:
|
|
318
|
-
# 1.
|
|
319
|
-
# 2.
|
|
320
|
-
# 3. does not contain any depreated, now invalid values (inline, eval, self, none)
|
|
367
|
+
# 1. is an array of strings
|
|
368
|
+
# 2. does not contain any deprecated, now invalid values (inline, eval, self, none)
|
|
321
369
|
#
|
|
322
370
|
# Does not validate the invididual values of the source expression (e.g.
|
|
323
371
|
# script_src => h*t*t*p: will not raise an exception)
|
|
324
372
|
def validate_source_expression!(directive, source_expression)
|
|
325
|
-
|
|
326
|
-
|
|
373
|
+
if source_expression != OPT_OUT
|
|
374
|
+
ensure_array_of_strings!(directive, source_expression)
|
|
375
|
+
end
|
|
327
376
|
ensure_valid_sources!(directive, source_expression)
|
|
328
377
|
end
|
|
329
378
|
|
|
@@ -333,8 +382,8 @@ module SecureHeaders
|
|
|
333
382
|
end
|
|
334
383
|
end
|
|
335
384
|
|
|
336
|
-
def ensure_array_of_strings!(directive,
|
|
337
|
-
|
|
385
|
+
def ensure_array_of_strings!(directive, value)
|
|
386
|
+
if (!value.is_a?(Array) || !value.compact.all? { |v| v.is_a?(String) })
|
|
338
387
|
raise ContentSecurityPolicyConfigError.new("#{directive} must be an array of strings")
|
|
339
388
|
end
|
|
340
389
|
end
|
|
@@ -41,10 +41,12 @@ module SecureHeaders
|
|
|
41
41
|
|
|
42
42
|
# when configuring with booleans, only one enforcement is permitted
|
|
43
43
|
def validate_samesite_boolean_config!
|
|
44
|
-
if config[:samesite].key?(:lax) && config[:samesite][:lax].is_a?(TrueClass) && config[:samesite].key?(:strict)
|
|
45
|
-
raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax
|
|
46
|
-
elsif config[:samesite].key?(:strict) && config[:samesite][:strict].is_a?(TrueClass) && config[:samesite].key?(:lax)
|
|
47
|
-
raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax
|
|
44
|
+
if config[:samesite].key?(:lax) && config[:samesite][:lax].is_a?(TrueClass) && (config[:samesite].key?(:strict) || config[:samesite].key?(:none))
|
|
45
|
+
raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax with strict or no enforcement is not permitted.")
|
|
46
|
+
elsif config[:samesite].key?(:strict) && config[:samesite][:strict].is_a?(TrueClass) && (config[:samesite].key?(:lax) || config[:samesite].key?(:none))
|
|
47
|
+
raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure strict with lax or no enforcement is not permitted.")
|
|
48
|
+
elsif config[:samesite].key?(:none) && config[:samesite][:none].is_a?(TrueClass) && (config[:samesite].key?(:lax) || config[:samesite].key?(:strict))
|
|
49
|
+
raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure no enforcement with lax or strict is not permitted.")
|
|
48
50
|
end
|
|
49
51
|
end
|
|
50
52
|
|
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.8.0"
|
|
5
5
|
gem.authors = ["Neil Matatall"]
|
|
6
6
|
gem.email = ["neil.matatall@gmail.com"]
|
|
7
7
|
gem.description = 'Manages application of security headers with many safe defaults.'
|
|
@@ -23,6 +23,15 @@ module SecureHeaders
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
describe "#value" do
|
|
26
|
+
it "uses a safe but non-breaking default value" do
|
|
27
|
+
expect(ContentSecurityPolicy.new.value).to eq("default-src https:")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it "deprecates and escapes semicolons in directive source lists" do
|
|
31
|
+
expect(Kernel).to receive(:warn).with("frame_ancestors contains a ; in 'google.com;script-src *;.;' which will raise an error in future versions. It has been replaced with a blank space.")
|
|
32
|
+
expect(ContentSecurityPolicy.new(frame_ancestors: %w(https://google.com;script-src https://*;.;)).value).to eq("frame-ancestors google.com script-src * .")
|
|
33
|
+
end
|
|
34
|
+
|
|
26
35
|
it "discards 'none' values if any other source expressions are present" do
|
|
27
36
|
csp = ContentSecurityPolicy.new(default_opts.merge(child_src: %w('self' 'none')))
|
|
28
37
|
expect(csp.value).not_to include("'none'")
|
|
@@ -86,6 +95,21 @@ module SecureHeaders
|
|
|
86
95
|
expect(csp.value).to eq("default-src example.org")
|
|
87
96
|
end
|
|
88
97
|
|
|
98
|
+
it "creates maximally strict sandbox policy when passed no sandbox token values" do
|
|
99
|
+
csp = ContentSecurityPolicy.new(default_src: %w(example.org), sandbox: [])
|
|
100
|
+
expect(csp.value).to eq("default-src example.org; sandbox")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it "creates maximally strict sandbox policy when passed true" do
|
|
104
|
+
csp = ContentSecurityPolicy.new(default_src: %w(example.org), sandbox: true)
|
|
105
|
+
expect(csp.value).to eq("default-src example.org; sandbox")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it "creates sandbox policy when passed valid sandbox token values" do
|
|
109
|
+
csp = ContentSecurityPolicy.new(default_src: %w(example.org), sandbox: %w(allow-forms allow-scripts))
|
|
110
|
+
expect(csp.value).to eq("default-src example.org; sandbox allow-forms allow-scripts")
|
|
111
|
+
end
|
|
112
|
+
|
|
89
113
|
it "does not emit a warning when using frame-src" do
|
|
90
114
|
expect(Kernel).to_not receive(:warn)
|
|
91
115
|
ContentSecurityPolicy.new(default_src: %w('self'), frame_src: %w('self')).value
|
|
@@ -120,50 +144,52 @@ module SecureHeaders
|
|
|
120
144
|
block_all_mixed_content: true,
|
|
121
145
|
upgrade_insecure_requests: true,
|
|
122
146
|
script_src: %w(script-src.com),
|
|
123
|
-
script_nonce: 123456
|
|
147
|
+
script_nonce: 123456,
|
|
148
|
+
sandbox: %w(allow-forms),
|
|
149
|
+
plugin_types: %w(application/pdf)
|
|
124
150
|
})
|
|
125
151
|
end
|
|
126
152
|
|
|
127
153
|
it "does not filter any directives for Chrome" do
|
|
128
154
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:chrome])
|
|
129
|
-
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types
|
|
155
|
+
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; worker-src worker-src.com; report-uri report-uri.com")
|
|
130
156
|
end
|
|
131
157
|
|
|
132
158
|
it "does not filter any directives for Opera" do
|
|
133
159
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:opera])
|
|
134
|
-
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types
|
|
160
|
+
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; worker-src worker-src.com; report-uri report-uri.com")
|
|
135
161
|
end
|
|
136
162
|
|
|
137
163
|
it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do
|
|
138
164
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox])
|
|
139
|
-
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; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox
|
|
165
|
+
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; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
|
|
140
166
|
end
|
|
141
167
|
|
|
142
168
|
it "filters blocked-all-mixed-content, frame-src, and plugin-types for firefox 46 and higher" do
|
|
143
169
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox46])
|
|
144
|
-
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox
|
|
170
|
+
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
|
|
145
171
|
end
|
|
146
172
|
|
|
147
173
|
it "child-src value is copied to frame-src, adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for Edge" do
|
|
148
174
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:edge])
|
|
149
|
-
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
|
|
175
|
+
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 allow-forms; script-src script-src.com 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
|
|
150
176
|
end
|
|
151
177
|
|
|
152
178
|
it "child-src value is copied to frame-src, adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for safari" do
|
|
153
179
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari6])
|
|
154
|
-
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
|
|
180
|
+
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 allow-forms; script-src script-src.com 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
|
|
155
181
|
end
|
|
156
182
|
|
|
157
183
|
it "adds 'unsafe-inline', filters blocked-all-mixed-content, upgrade-insecure-requests, nonce sources, and hash sources for safari 10 and higher" do
|
|
158
184
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari10])
|
|
159
|
-
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; plugin-types
|
|
185
|
+
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; report-uri report-uri.com")
|
|
160
186
|
end
|
|
161
187
|
|
|
162
188
|
it "falls back to standard Firefox defaults when the useragent version is not present" do
|
|
163
189
|
ua = USER_AGENTS[:firefox].dup
|
|
164
190
|
allow(ua).to receive(:version).and_return(nil)
|
|
165
191
|
policy = ContentSecurityPolicy.new(complex_opts, ua)
|
|
166
|
-
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; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox
|
|
192
|
+
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; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
|
|
167
193
|
end
|
|
168
194
|
end
|
|
169
195
|
end
|
|
@@ -62,29 +62,21 @@ module SecureHeaders
|
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
context "SameSite cookies" do
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
it "flags SameSite=Lax when configured with a boolean" do
|
|
71
|
-
cookie = Cookie.new(raw_cookie, samesite: { lax: true})
|
|
72
|
-
expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Lax")
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
it "does not flag cookies as SameSite=Lax when excluded" do
|
|
76
|
-
cookie = Cookie.new(raw_cookie, samesite: { lax: { except: ["_session"] } })
|
|
77
|
-
expect(cookie.to_s).to eq("_session=thisisatest")
|
|
78
|
-
end
|
|
65
|
+
%w(None Lax Strict).each do |flag|
|
|
66
|
+
it "flags SameSite=#{flag}" do
|
|
67
|
+
cookie = Cookie.new(raw_cookie, samesite: { flag.downcase.to_sym => { only: ["_session"] } })
|
|
68
|
+
expect(cookie.to_s).to eq("_session=thisisatest; SameSite=#{flag}")
|
|
69
|
+
end
|
|
79
70
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
71
|
+
it "flags SameSite=#{flag} when configured with a boolean" do
|
|
72
|
+
cookie = Cookie.new(raw_cookie, samesite: { flag.downcase.to_sym => true })
|
|
73
|
+
expect(cookie.to_s).to eq("_session=thisisatest; SameSite=#{flag}")
|
|
74
|
+
end
|
|
84
75
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
76
|
+
it "does not flag cookies as SameSite=#{flag} when excluded" do
|
|
77
|
+
cookie = Cookie.new(raw_cookie, samesite: { flag.downcase.to_sym => { except: ["_session"] } })
|
|
78
|
+
expect(cookie.to_s).to eq("_session=thisisatest")
|
|
79
|
+
end
|
|
88
80
|
end
|
|
89
81
|
|
|
90
82
|
it "flags SameSite=Strict when configured with a boolean" do
|
|
@@ -131,10 +123,15 @@ module SecureHeaders
|
|
|
131
123
|
end.to raise_error(CookiesConfigError)
|
|
132
124
|
end
|
|
133
125
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
126
|
+
cookie_options = %w(none lax strict).map(&:to_sym)
|
|
127
|
+
cookie_options.each do |flag|
|
|
128
|
+
(cookie_options - [flag]).each do |other_flag|
|
|
129
|
+
it "raises an exception when SameSite #{flag} and #{other_flag} enforcement modes are configured with booleans" do
|
|
130
|
+
expect do
|
|
131
|
+
Cookie.validate_config!(samesite: { flag => true, other_flag => true})
|
|
132
|
+
end.to raise_error(CookiesConfigError)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
138
135
|
end
|
|
139
136
|
|
|
140
137
|
it "raises an exception when SameSite lax and strict enforcement modes are configured with booleans" do
|
|
@@ -3,13 +3,13 @@ require "spec_helper"
|
|
|
3
3
|
|
|
4
4
|
module SecureHeaders
|
|
5
5
|
describe ExpectCertificateTransparency do
|
|
6
|
-
specify { expect(ExpectCertificateTransparency.new(max_age: 1234, enforce: true).value).to eq("enforce
|
|
6
|
+
specify { expect(ExpectCertificateTransparency.new(max_age: 1234, enforce: true).value).to eq("enforce, max-age=1234") }
|
|
7
7
|
specify { expect(ExpectCertificateTransparency.new(max_age: 1234, enforce: false).value).to eq("max-age=1234") }
|
|
8
8
|
specify { expect(ExpectCertificateTransparency.new(max_age: 1234, enforce: "yolocopter").value).to eq("max-age=1234") }
|
|
9
|
-
specify { expect(ExpectCertificateTransparency.new(max_age: 1234, report_uri: "https://report-uri.io/expect-ct").value).to eq("max-age=1234
|
|
9
|
+
specify { expect(ExpectCertificateTransparency.new(max_age: 1234, report_uri: "https://report-uri.io/expect-ct").value).to eq("max-age=1234, report-uri=\"https://report-uri.io/expect-ct\"") }
|
|
10
10
|
specify do
|
|
11
11
|
config = { enforce: true, max_age: 1234, report_uri: "https://report-uri.io/expect-ct" }
|
|
12
|
-
header_value = "enforce
|
|
12
|
+
header_value = "enforce, max-age=1234, report-uri=\"https://report-uri.io/expect-ct\""
|
|
13
13
|
expect(ExpectCertificateTransparency.new(config).value).to eq(header_value)
|
|
14
14
|
end
|
|
15
15
|
|
|
@@ -32,6 +32,7 @@ module SecureHeaders
|
|
|
32
32
|
object_src: %w('self'),
|
|
33
33
|
script_src: %w('self'),
|
|
34
34
|
style_src: %w('unsafe-inline'),
|
|
35
|
+
worker_src: %w(worker.com),
|
|
35
36
|
base_uri: %w('self'),
|
|
36
37
|
form_action: %w('self' github.com),
|
|
37
38
|
frame_ancestors: %w('none'),
|
|
@@ -98,6 +99,36 @@ module SecureHeaders
|
|
|
98
99
|
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w(self none inline eval)))
|
|
99
100
|
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
100
101
|
end
|
|
102
|
+
|
|
103
|
+
it "rejects anything not of the form allow-* as a sandbox value" do
|
|
104
|
+
expect do
|
|
105
|
+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(sandbox: ["steve"])))
|
|
106
|
+
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it "accepts anything of the form allow-* as a sandbox value " do
|
|
110
|
+
expect do
|
|
111
|
+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(sandbox: ["allow-foo"])))
|
|
112
|
+
end.to_not raise_error
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it "accepts true as a sandbox policy" do
|
|
116
|
+
expect do
|
|
117
|
+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(sandbox: true)))
|
|
118
|
+
end.to_not raise_error
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it "rejects anything not of the form type/subtype as a plugin-type value" do
|
|
122
|
+
expect do
|
|
123
|
+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(plugin_types: ["steve"])))
|
|
124
|
+
end.to raise_error(ContentSecurityPolicyConfigError)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it "accepts anything of the form type/subtype as a plugin-type value " do
|
|
128
|
+
expect do
|
|
129
|
+
ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(plugin_types: ["application/pdf"])))
|
|
130
|
+
end.to_not raise_error
|
|
131
|
+
end
|
|
101
132
|
end
|
|
102
133
|
|
|
103
134
|
describe "#combine_policies" do
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: secure_headers
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Neil Matatall
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2020-01-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rake
|
|
@@ -93,7 +93,7 @@ files:
|
|
|
93
93
|
- spec/lib/secure_headers/headers/clear_site_data_spec.rb
|
|
94
94
|
- spec/lib/secure_headers/headers/content_security_policy_spec.rb
|
|
95
95
|
- spec/lib/secure_headers/headers/cookie_spec.rb
|
|
96
|
-
- spec/lib/secure_headers/headers/
|
|
96
|
+
- spec/lib/secure_headers/headers/expect_certificate_transparency_spec.rb
|
|
97
97
|
- spec/lib/secure_headers/headers/policy_management_spec.rb
|
|
98
98
|
- spec/lib/secure_headers/headers/public_key_pins_spec.rb
|
|
99
99
|
- spec/lib/secure_headers/headers/referrer_policy_spec.rb
|
|
@@ -127,8 +127,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
127
127
|
- !ruby/object:Gem::Version
|
|
128
128
|
version: '0'
|
|
129
129
|
requirements: []
|
|
130
|
-
|
|
131
|
-
rubygems_version: 2.5.2
|
|
130
|
+
rubygems_version: 3.1.2
|
|
132
131
|
signing_key:
|
|
133
132
|
specification_version: 4
|
|
134
133
|
summary: Add easily configured security headers to responses including content-security-policy,
|
|
@@ -138,7 +137,7 @@ test_files:
|
|
|
138
137
|
- spec/lib/secure_headers/headers/clear_site_data_spec.rb
|
|
139
138
|
- spec/lib/secure_headers/headers/content_security_policy_spec.rb
|
|
140
139
|
- spec/lib/secure_headers/headers/cookie_spec.rb
|
|
141
|
-
- spec/lib/secure_headers/headers/
|
|
140
|
+
- spec/lib/secure_headers/headers/expect_certificate_transparency_spec.rb
|
|
142
141
|
- spec/lib/secure_headers/headers/policy_management_spec.rb
|
|
143
142
|
- spec/lib/secure_headers/headers/public_key_pins_spec.rb
|
|
144
143
|
- spec/lib/secure_headers/headers/referrer_policy_spec.rb
|