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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 591de0f41bc64b3520fbb2fdab9fc94b490d1604
4
- data.tar.gz: f836c5e63e4d054ff2ce3f1ec597bbd9cf67ed4b
2
+ SHA256:
3
+ metadata.gz: d605beb08177e1325edd0726b0cb551d3ac71b6dc6da7c1261d6f30b60db7695
4
+ data.tar.gz: f166e3735db90264366d654520269fb3cfb696ce9747c1ce92c11927d3322cb2
5
5
  SHA512:
6
- metadata.gz: 5317d425d7c65dfc73b32703aae7e16e3a566b620b42e3233cd6dc7403a1a9842bb7dc00ffc5c13a86240c5a8abee3ae07fed0c73d4b06a06bcc11db39a79a20
7
- data.tar.gz: 154573bfb9112b4b1510abbde3f5eb1e85b709f7839ff4a6a547223e67e698ff014bb578f02b0e8775ee723016f5903e8cda38cb9846b24cafe3dc8e414bb007
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 `Lax` enforcement modes can also be specified using a Hash.
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 :string
84
- [symbol_to_hyphen_case(directive_name), @config.directive_value(directive_name)].join(" ")
85
- else
86
- build_directive(directive_name)
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 build_directive(directive)
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
- [symbol_to_hyphen_case(directive), normalized_source_list].join(" ")
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.
@@ -37,6 +37,7 @@ module SecureHeaders
37
37
  @script_src = nil
38
38
  @style_nonce = nil
39
39
  @style_src = nil
40
+ @worker_src = nil
40
41
  @upgrade_insecure_requests = nil
41
42
 
42
43
  from_hash(hash)
@@ -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
 
@@ -49,7 +49,7 @@ module SecureHeaders
49
49
  enforced_directive,
50
50
  max_age_directive,
51
51
  report_uri_directive
52
- ].compact.join("; ").strip
52
+ ].compact.join(", ").strip
53
53
  end
54
54
 
55
55
  def enforced_directive
@@ -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 => :source_list,
144
+ PLUGIN_TYPES => :media_type_list,
152
145
  REPORT_URI => :source_list,
153
- SANDBOX => :source_list,
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 source_list?(directive)
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
- if !original[directive] && ((source_list?(directive) && FETCH_SOURCES.include?(directive)) || nonce_added?(original, additions))
270
- if nonce_added?(original, additions)
271
- inferred_directive = directive.to_s.gsub(/_nonce/, "_src").to_sym
272
- unless original[inferred_directive] || NON_FETCH_SOURCES.include?(inferred_directive)
273
- original[inferred_directive] = default_for(directive, original)
274
- end
275
- else
276
- original[directive] = default_for(directive, original)
277
- end
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, source_expression)
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?(source_expression)
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
- validate_source_expression!(directive, source_expression)
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. has a valid name
319
- # 2. is an array of strings
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
- ensure_valid_directive!(directive)
326
- ensure_array_of_strings!(directive, source_expression)
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, source_expression)
337
- unless source_expression.is_a?(Array) && source_expression.compact.all? { |v| v.is_a?(String) }
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 and strict enforcement is not permitted.")
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 and strict enforcement is not permitted.")
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
 
@@ -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.7.0"
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 plugin-types.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
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 plugin-types.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
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 sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
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 sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
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 sandbox.com; script-src script-src.com 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
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 sandbox.com; script-src script-src.com 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
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 plugin-types.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; report-uri report-uri.com")
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 sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
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
- it "flags SameSite=Lax" do
66
- cookie = Cookie.new(raw_cookie, samesite: { lax: { only: ["_session"] } })
67
- expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Lax")
68
- end
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
- it "flags SameSite=Strict" do
81
- cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] } })
82
- expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Strict")
83
- end
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
- it "does not flag cookies as SameSite=Strict when excluded" do
86
- cookie = Cookie.new(raw_cookie, samesite: { strict: { except: ["_session"] } })
87
- expect(cookie.to_s).to eq("_session=thisisatest")
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
- it "raises an exception when SameSite lax and strict enforcement modes are configured with booleans" do
135
- expect do
136
- Cookie.validate_config!(samesite: { lax: true, strict: true})
137
- end.to raise_error(CookiesConfigError)
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; max-age=1234") }
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; report-uri=\"https://report-uri.io/expect-ct\"") }
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; max-age=1234; report-uri=\"https://report-uri.io/expect-ct\""
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.7.0
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: 2017-08-25 00:00:00.000000000 Z
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/expect_certificate_spec.rb
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
- rubyforge_project:
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/expect_certificate_spec.rb
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