secure_headers 4.0.0.alpha03 → 4.0.0.alpha04

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of secure_headers might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 85aab6315aa227ca4e8e7814e1044fc43f4d0d3d
4
- data.tar.gz: 6cbb3f6f3fec29168f3213d794babc76143c3201
3
+ metadata.gz: 96b8466fa2b4e4400b0c587bdba7abebdac251f7
4
+ data.tar.gz: f721a9682de31fc461dd713e153cba427f846b51
5
5
  SHA512:
6
- metadata.gz: 6e767caf78326e1ed60678dfd22f4f5f8328fbb30dc520d5283f86c018708c3be2326ffb79df585453f7f995771dc8521e1c6ca48e0af787620a60b5de831d26
7
- data.tar.gz: 1b699528fdcd23ab18921c825e646fd8de323c3d0497b3e482e99386f5ac008bd948ffc1889e9c32f82cbd6b8d40c74c62a18fbeee87622aebc34b043a86c2a4
6
+ metadata.gz: 29125ef959986803eff014bfd101972752272c97a5170356961dde54b78c4bcafdb5dc6fc55f99cd49f46b5c29c247e63161da9af6932fdb136d690f1dc2a598
7
+ data.tar.gz: b23ef45067291c21c66b735ebe19c454e8b35b5d829324ea35e33a4cb8018fad7e2a53081508334d9483bb7ff86cd9ce90b053f1c4b9052bad8c0d991d9d7f6e
data/CHANGELOG.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ## 4.x
2
2
 
3
- - See the [upgrading to 4.0](upgrading-to-4.0.md) guide. Lots of breaking changes.
3
+ - See the [upgrading to 4.0](upgrading-to-4-0.md) guide. Lots of breaking changes.
4
4
 
5
5
  ## 3.6.5
6
6
 
data/README.md CHANGED
@@ -19,6 +19,7 @@ The gem will automatically apply several headers that are related to security.
19
19
  - X-Permitted-Cross-Domain-Policies - [Restrict Adobe Flash Player's access to data](https://www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html)
20
20
  - Referrer-Policy - [Referrer Policy draft](https://w3c.github.io/webappsec-referrer-policy/)
21
21
  - Public Key Pinning - Pin certificate fingerprints in the browser to prevent man-in-the-middle attacks due to compromised Certificate Authorities. [Public Key Pinning Specification](https://tools.ietf.org/html/rfc7469)
22
+ - Expect-CT - Only use certificates that are present in the certificate transparency logs. [Expect-CT draft specification](https://datatracker.ietf.org/doc/draft-stark-expect-ct/).
22
23
  - Clear-Site-Data - Clearing browser data for origin. [Clear-Site-Data specification](https://w3c.github.io/webappsec-clear-site-data/).
23
24
 
24
25
  It can also mark all http cookies with the Secure, HttpOnly and SameSite attributes (when configured to do so).
@@ -77,6 +78,11 @@ SecureHeaders::Configuration.default do |config|
77
78
  "storage",
78
79
  "executionContexts"
79
80
  ]
81
+ config.expect_certificate_transparency = {
82
+ enforce: false,
83
+ max_age: 1.day.to_i,
84
+ report_uri: "https://report-uri.io/example-ct"
85
+ }
80
86
  config.csp = {
81
87
  # "meta" values. these will shape the header, but the values are not included in the header.
82
88
  preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content.
@@ -94,6 +100,7 @@ SecureHeaders::Configuration.default do |config|
94
100
  manifest_src: %w('self'),
95
101
  media_src: %w(utoob.com),
96
102
  object_src: %w('self'),
103
+ sandbox: true, # true and [] will set a maximally restrictive setting
97
104
  plugin_types: %w(application/x-shockwave-flash),
98
105
  script_src: %w('self'),
99
106
  style_src: %w('unsafe-inline'),
@@ -12,6 +12,7 @@ require "secure_headers/headers/x_download_options"
12
12
  require "secure_headers/headers/x_permitted_cross_domain_policies"
13
13
  require "secure_headers/headers/referrer_policy"
14
14
  require "secure_headers/headers/clear_site_data"
15
+ require "secure_headers/headers/expect_certificate_transparency"
15
16
  require "secure_headers/middleware"
16
17
  require "secure_headers/railtie"
17
18
  require "secure_headers/view_helper"
@@ -52,6 +53,7 @@ module SecureHeaders
52
53
  CSP = ContentSecurityPolicy
53
54
 
54
55
  ALL_HEADER_CLASSES = [
56
+ ExpectCertificateTransparency,
55
57
  ClearSiteData,
56
58
  ContentSecurityPolicyConfig,
57
59
  ContentSecurityPolicyReportOnlyConfig,
@@ -117,7 +117,7 @@ module SecureHeaders
117
117
 
118
118
  attr_writer :hsts, :x_frame_options, :x_content_type_options,
119
119
  :x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies,
120
- :referrer_policy, :clear_site_data
120
+ :referrer_policy, :clear_site_data, :expect_certificate_transparency
121
121
 
122
122
  attr_reader :cached_headers, :csp, :cookies, :csp_report_only, :hpkp, :hpkp_report_host
123
123
 
@@ -144,6 +144,7 @@ module SecureHeaders
144
144
  @x_frame_options = nil
145
145
  @x_permitted_cross_domain_policies = nil
146
146
  @x_xss_protection = nil
147
+ @expect_certificate_transparency = nil
147
148
 
148
149
  self.hpkp = OPT_OUT
149
150
  self.referrer_policy = OPT_OUT
@@ -169,6 +170,7 @@ module SecureHeaders
169
170
  copy.x_download_options = @x_download_options
170
171
  copy.x_permitted_cross_domain_policies = @x_permitted_cross_domain_policies
171
172
  copy.clear_site_data = @clear_site_data
173
+ copy.expect_certificate_transparency = @expect_certificate_transparency
172
174
  copy.referrer_policy = @referrer_policy
173
175
  copy.hpkp = @hpkp
174
176
  copy.hpkp_report_host = @hpkp_report_host
@@ -201,6 +203,7 @@ module SecureHeaders
201
203
  XDownloadOptions.validate_config!(@x_download_options)
202
204
  XPermittedCrossDomainPolicies.validate_config!(@x_permitted_cross_domain_policies)
203
205
  ClearSiteData.validate_config!(@clear_site_data)
206
+ ExpectCertificateTransparency.validate_config!(@expect_certificate_transparency)
204
207
  PublicKeyPins.validate_config!(@hpkp)
205
208
  Cookie.validate_config!(@cookies)
206
209
  end
@@ -75,20 +75,55 @@ module SecureHeaders
75
75
  case DIRECTIVE_VALUE_TYPES[directive_name]
76
76
  when :boolean
77
77
  symbol_to_hyphen_case(directive_name) if @config.directive_value(directive_name)
78
- when :string
79
- [symbol_to_hyphen_case(directive_name), @config.directive_value(directive_name)].join(" ")
80
- else
81
- build_directive(directive_name)
78
+ when :sandbox_list
79
+ build_sandbox_list_directive(directive_name)
80
+ when :media_type_list
81
+ build_media_type_list_directive(directive_name)
82
+ when :source_list
83
+ build_source_list_directive(directive_name)
82
84
  end
83
85
  end.compact.join("; ")
84
86
  end
85
87
 
88
+ def build_sandbox_list_directive(directive)
89
+ return unless sandbox_list = @config.directive_value(directive)
90
+ max_strict_policy = case sandbox_list
91
+ when Array
92
+ sandbox_list.empty?
93
+ when true
94
+ true
95
+ else
96
+ false
97
+ end
98
+
99
+ # A maximally strict sandbox policy is just the `sandbox` directive,
100
+ # whith no configuraiton values.
101
+ if max_strict_policy
102
+ symbol_to_hyphen_case(directive)
103
+ elsif sandbox_list && sandbox_list.any?
104
+ [
105
+ symbol_to_hyphen_case(directive),
106
+ sandbox_list.uniq
107
+ ].join(" ")
108
+ end
109
+ end
110
+
111
+ def build_media_type_list_directive(directive)
112
+ return unless media_type_list = @config.directive_value(directive)
113
+ if media_type_list && media_type_list.any?
114
+ [
115
+ symbol_to_hyphen_case(directive),
116
+ media_type_list.uniq
117
+ ].join(" ")
118
+ end
119
+ end
120
+
86
121
  # Private: builds a string that represents one directive in a minified form.
87
122
  #
88
123
  # directive_name - a symbol representing the various ALL_DIRECTIVES
89
124
  #
90
125
  # Returns a string representing a directive.
91
- def build_directive(directive)
126
+ def build_source_list_directive(directive)
92
127
  source_list = case directive
93
128
  when :child_src
94
129
  if supported_directives.include?(:child_src)
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+ module SecureHeaders
3
+ class ExpectCertificateTransparencyConfigError < StandardError; end
4
+
5
+ class ExpectCertificateTransparency
6
+ HEADER_NAME = "Expect-CT".freeze
7
+ CONFIG_KEY = :expect_certificate_transparency
8
+ INVALID_CONFIGURATION_ERROR = "config must be a hash.".freeze
9
+ INVALID_ENFORCE_VALUE_ERROR = "enforce must be a boolean".freeze
10
+ REQUIRED_MAX_AGE_ERROR = "max-age is a required directive.".freeze
11
+ INVALID_MAX_AGE_ERROR = "max-age must be a number.".freeze
12
+
13
+ class << self
14
+ # Public: Generate a Expect-CT header.
15
+ #
16
+ # Returns nil if not configured, returns header name and value if
17
+ # configured.
18
+ def make_header(config)
19
+ return if config.nil?
20
+
21
+ header = new(config)
22
+ [HEADER_NAME, header.value]
23
+ end
24
+
25
+ def validate_config!(config)
26
+ return if config.nil? || config == OPT_OUT
27
+ raise ExpectCertificateTransparencyConfigError.new(INVALID_CONFIGURATION_ERROR) unless config.is_a? Hash
28
+
29
+ unless [true, false, nil].include?(config[:enforce])
30
+ raise ExpectCertificateTransparencyConfigError.new(INVALID_ENFORCE_VALUE_ERROR)
31
+ end
32
+
33
+ if !config[:max_age]
34
+ raise ExpectCertificateTransparencyConfigError.new(REQUIRED_MAX_AGE_ERROR)
35
+ elsif config[:max_age].to_s !~ /\A\d+\z/
36
+ raise ExpectCertificateTransparencyConfigError.new(INVALID_MAX_AGE_ERROR)
37
+ end
38
+ end
39
+ end
40
+
41
+ def initialize(config)
42
+ @enforced = config.fetch(:enforce, nil)
43
+ @max_age = config.fetch(:max_age, nil)
44
+ @report_uri = config.fetch(:report_uri, nil)
45
+ end
46
+
47
+ def value
48
+ [
49
+ enforced_directive,
50
+ max_age_directive,
51
+ report_uri_directive
52
+ ].compact.join("; ").strip
53
+ end
54
+
55
+ def enforced_directive
56
+ # Unfortunately `if @enforced` isn't enough here in case someone
57
+ # passes in a random string so let's be specific with it to prevent
58
+ # accidental enforcement.
59
+ "enforce" if @enforced == true
60
+ end
61
+
62
+ def max_age_directive
63
+ "max-age=#{@max_age}" if @max_age
64
+ end
65
+
66
+ def report_uri_directive
67
+ "report-uri=\"#{@report_uri}\"" if @report_uri
68
+ end
69
+ end
70
+ end
@@ -116,18 +116,6 @@ module SecureHeaders
116
116
  # everything else is in between.
117
117
  BODY_DIRECTIVES = ALL_DIRECTIVES - [DEFAULT_SRC, REPORT_URI]
118
118
 
119
- # These are directives that do not inherit the default-src value. This is
120
- # useful when calling #combine_policies.
121
- NON_FETCH_SOURCES = [
122
- BASE_URI,
123
- FORM_ACTION,
124
- FRAME_ANCESTORS,
125
- PLUGIN_TYPES,
126
- REPORT_URI
127
- ]
128
-
129
- FETCH_SOURCES = ALL_DIRECTIVES - NON_FETCH_SOURCES
130
-
131
119
  VARIATIONS = {
132
120
  "Chrome" => CHROME_DIRECTIVES,
133
121
  "Opera" => CHROME_DIRECTIVES,
@@ -155,14 +143,30 @@ module SecureHeaders
155
143
  MANIFEST_SRC => :source_list,
156
144
  MEDIA_SRC => :source_list,
157
145
  OBJECT_SRC => :source_list,
158
- PLUGIN_TYPES => :source_list,
146
+ PLUGIN_TYPES => :media_type_list,
159
147
  REPORT_URI => :source_list,
160
- SANDBOX => :source_list,
148
+ SANDBOX => :sandbox_list,
161
149
  SCRIPT_SRC => :source_list,
162
150
  STYLE_SRC => :source_list,
163
151
  UPGRADE_INSECURE_REQUESTS => :boolean
164
152
  }.freeze
165
153
 
154
+ # These are directives that don't have use a source list, and hence do not
155
+ # inherit the default-src value.
156
+ NON_SOURCE_LIST_SOURCES = DIRECTIVE_VALUE_TYPES.select do |_, type|
157
+ type != :source_list
158
+ end.keys.freeze
159
+
160
+ # These are directives that take a source list, but that do not inherit
161
+ # the default-src value.
162
+ NON_FETCH_SOURCES = [
163
+ BASE_URI,
164
+ FORM_ACTION,
165
+ FRAME_ANCESTORS,
166
+ REPORT_URI
167
+ ]
168
+
169
+ FETCH_SOURCES = ALL_DIRECTIVES - NON_FETCH_SOURCES - NON_SOURCE_LIST_SOURCES
166
170
 
167
171
  STAR_REGEXP = Regexp.new(Regexp.escape(STAR))
168
172
  HTTP_SCHEME_REGEX = %r{\Ahttps?://}
@@ -264,7 +268,7 @@ module SecureHeaders
264
268
  # when each hash contains a value for a given key.
265
269
  def merge_policy_additions(original, additions)
266
270
  original.merge(additions) do |directive, lhs, rhs|
267
- if source_list?(directive)
271
+ if list_directive?(directive)
268
272
  (lhs.to_a + rhs.to_a).compact.uniq
269
273
  else
270
274
  rhs
@@ -272,20 +276,27 @@ module SecureHeaders
272
276
  end.reject { |_, value| value.nil? || value == [] } # this mess prevents us from adding empty directives.
273
277
  end
274
278
 
279
+ # Returns True if a directive expects a list of values and False otherwise.
280
+ def list_directive?(directive)
281
+ source_list?(directive) ||
282
+ sandbox_list?(directive) ||
283
+ media_type_list?(directive)
284
+ end
285
+
275
286
  # For each directive in additions that does not exist in the original config,
276
287
  # copy the default-src value to the original config. This modifies the original hash.
277
288
  def populate_fetch_source_with_default!(original, additions)
278
289
  # in case we would be appending to an empty directive, fill it with the default-src value
279
290
  additions.each_key do |directive|
280
- if !original[directive] && ((source_list?(directive) && FETCH_SOURCES.include?(directive)) || nonce_added?(original, additions))
281
- if nonce_added?(original, additions)
282
- inferred_directive = directive.to_s.gsub(/_nonce/, "_src").to_sym
283
- unless original[inferred_directive] || NON_FETCH_SOURCES.include?(inferred_directive)
284
- original[inferred_directive] = default_for(directive, original)
285
- end
286
- else
287
- original[directive] = default_for(directive, original)
288
- end
291
+ directive = if directive.to_s.end_with?("_nonce")
292
+ directive.to_s.gsub(/_nonce/, "_src").to_sym
293
+ else
294
+ directive
295
+ end
296
+ # Don't set a default if directive has an existing value
297
+ next if original[directive]
298
+ if FETCH_SOURCES.include?(directive)
299
+ original[directive] = default_for(directive, original)
289
300
  end
290
301
  end
291
302
  end
@@ -296,45 +307,77 @@ module SecureHeaders
296
307
  original[DEFAULT_SRC]
297
308
  end
298
309
 
299
- def nonce_added?(original, additions)
300
- [:script_nonce, :style_nonce].each do |nonce|
301
- if additions[nonce] && !original[nonce]
302
- return true
303
- end
304
- end
305
- end
306
-
307
310
  def source_list?(directive)
308
311
  DIRECTIVE_VALUE_TYPES[directive] == :source_list
309
312
  end
310
313
 
314
+ def sandbox_list?(directive)
315
+ DIRECTIVE_VALUE_TYPES[directive] == :sandbox_list
316
+ end
317
+
318
+ def media_type_list?(directive)
319
+ DIRECTIVE_VALUE_TYPES[directive] == :media_type_list
320
+ end
321
+
311
322
  # Private: Validates that the configuration has a valid type, or that it is a valid
312
323
  # source expression.
313
- def validate_directive!(directive, source_expression)
324
+ def validate_directive!(directive, value)
325
+ ensure_valid_directive!(directive)
314
326
  case ContentSecurityPolicy::DIRECTIVE_VALUE_TYPES[directive]
315
327
  when :boolean
316
- unless boolean?(source_expression)
317
- raise ContentSecurityPolicyConfigError.new("#{directive} must be a boolean value")
318
- end
319
- when :string
320
- unless source_expression.is_a?(String)
321
- raise ContentSecurityPolicyConfigError.new("#{directive} Must be a string. Found #{config.class}: #{config} value")
328
+ unless boolean?(value)
329
+ raise ContentSecurityPolicyConfigError.new("#{directive} must be a boolean. Found #{value.class} value")
322
330
  end
331
+ when :sandbox_list
332
+ validate_sandbox_expression!(directive, value)
333
+ when :media_type_list
334
+ validate_media_type_expression!(directive, value)
335
+ when :source_list
336
+ validate_source_expression!(directive, value)
323
337
  else
324
- validate_source_expression!(directive, source_expression)
338
+ raise ContentSecurityPolicyConfigError.new("Unknown directive #{directive}")
339
+ end
340
+ end
341
+
342
+ # Private: validates that a sandbox token expression:
343
+ # 1. is an array of strings or optionally `true` (to enable maximal sandboxing)
344
+ # 2. For arrays, each element is of the form allow-*
345
+ def validate_sandbox_expression!(directive, sandbox_token_expression)
346
+ # We support sandbox: true to indicate a maximally secure sandbox.
347
+ return if boolean?(sandbox_token_expression) && sandbox_token_expression == true
348
+ ensure_array_of_strings!(directive, sandbox_token_expression)
349
+ valid = sandbox_token_expression.compact.all? do |v|
350
+ v.is_a?(String) && v.start_with?("allow-")
351
+ end
352
+ if !valid
353
+ raise ContentSecurityPolicyConfigError.new("#{directive} must be True or an array of zero or more sandbox token strings (ex. allow-forms)")
354
+ end
355
+ end
356
+
357
+ # Private: validates that a media type expression:
358
+ # 1. is an array of strings
359
+ # 2. each element is of the form type/subtype
360
+ def validate_media_type_expression!(directive, media_type_expression)
361
+ ensure_array_of_strings!(directive, media_type_expression)
362
+ valid = media_type_expression.compact.all? do |v|
363
+ # All media types are of the form: <type from RFC 2045> "/" <subtype from RFC 2045>.
364
+ v =~ /\A.+\/.+\z/
365
+ end
366
+ if !valid
367
+ raise ContentSecurityPolicyConfigError.new("#{directive} must be an array of valid media types (ex. application/pdf)")
325
368
  end
326
369
  end
327
370
 
328
371
  # Private: validates that a source expression:
329
- # 1. has a valid name
330
- # 2. is an array of strings
331
- # 3. does not contain any depreated, now invalid values (inline, eval, self, none)
372
+ # 1. is an array of strings
373
+ # 2. does not contain any deprecated, now invalid values (inline, eval, self, none)
332
374
  #
333
375
  # Does not validate the invididual values of the source expression (e.g.
334
376
  # script_src => h*t*t*p: will not raise an exception)
335
377
  def validate_source_expression!(directive, source_expression)
336
- ensure_valid_directive!(directive)
337
- ensure_array_of_strings!(directive, source_expression)
378
+ if source_expression != OPT_OUT
379
+ ensure_array_of_strings!(directive, source_expression)
380
+ end
338
381
  ensure_valid_sources!(directive, source_expression)
339
382
  end
340
383
 
@@ -344,8 +387,8 @@ module SecureHeaders
344
387
  end
345
388
  end
346
389
 
347
- def ensure_array_of_strings!(directive, source_expression)
348
- if (!source_expression.is_a?(Array) || !source_expression.compact.all? { |v| v.is_a?(String) }) && source_expression != OPT_OUT
390
+ def ensure_array_of_strings!(directive, value)
391
+ if (!value.is_a?(Array) || !value.compact.all? { |v| v.is_a?(String) })
349
392
  raise ContentSecurityPolicyConfigError.new("#{directive} must be an array of strings")
350
393
  end
351
394
  end
@@ -2,7 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
  Gem::Specification.new do |gem|
4
4
  gem.name = "secure_headers"
5
- gem.version = "4.0.0.alpha03"
5
+ gem.version = "4.0.0.alpha04"
6
6
  gem.authors = ["Neil Matatall"]
7
7
  gem.email = ["neil.matatall@gmail.com"]
8
8
  gem.description = "Manages application of security headers with many safe defaults."
@@ -96,6 +96,21 @@ module SecureHeaders
96
96
  expect(csp.value).to eq("default-src example.org")
97
97
  end
98
98
 
99
+ it "creates maximally strict sandbox policy when passed no sandbox token values" do
100
+ csp = ContentSecurityPolicy.new(default_src: %w(example.org), sandbox: [])
101
+ expect(csp.value).to eq("default-src example.org; sandbox")
102
+ end
103
+
104
+ it "creates maximally strict sandbox policy when passed true" do
105
+ csp = ContentSecurityPolicy.new(default_src: %w(example.org), sandbox: true)
106
+ expect(csp.value).to eq("default-src example.org; sandbox")
107
+ end
108
+
109
+ it "creates sandbox policy when passed valid sandbox token values" do
110
+ csp = ContentSecurityPolicy.new(default_src: %w(example.org), sandbox: %w(allow-forms allow-scripts))
111
+ expect(csp.value).to eq("default-src example.org; sandbox allow-forms allow-scripts")
112
+ end
113
+
99
114
  it "does not emit a warning when using frame-src" do
100
115
  expect(Kernel).to_not receive(:warn)
101
116
  ContentSecurityPolicy.new(default_src: %w('self'), frame_src: %w('self')).value
@@ -120,50 +135,52 @@ module SecureHeaders
120
135
  block_all_mixed_content: true,
121
136
  upgrade_insecure_requests: true,
122
137
  script_src: %w(script-src.com),
123
- script_nonce: 123456
138
+ script_nonce: 123456,
139
+ sandbox: %w(allow-forms),
140
+ plugin_types: %w(application/pdf)
124
141
  })
125
142
  end
126
143
 
127
144
  it "does not filter any directives for Chrome" do
128
145
  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")
146
+ 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; report-uri report-uri.com")
130
147
  end
131
148
 
132
149
  it "does not filter any directives for Opera" do
133
150
  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")
151
+ 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; report-uri report-uri.com")
135
152
  end
136
153
 
137
154
  it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do
138
155
  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")
156
+ 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
157
  end
141
158
 
142
159
  it "filters blocked-all-mixed-content, frame-src, and plugin-types for firefox 46 and higher" do
143
160
  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")
161
+ 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
162
  end
146
163
 
147
164
  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
165
  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")
166
+ 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
167
  end
151
168
 
152
169
  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
170
  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")
171
+ 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
172
  end
156
173
 
157
174
  it "adds 'unsafe-inline', filters blocked-all-mixed-content, upgrade-insecure-requests, nonce sources, and hash sources for safari 10 and higher" do
158
175
  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")
176
+ 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
177
  end
161
178
 
162
179
  it "falls back to standard Firefox defaults when the useragent version is not present" do
163
180
  ua = USER_AGENTS[:firefox].dup
164
181
  allow(ua).to receive(:version).and_return(nil)
165
182
  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")
183
+ 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
184
  end
168
185
  end
169
186
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+ require "spec_helper"
3
+
4
+ module SecureHeaders
5
+ describe ExpectCertificateTransparency do
6
+ specify { expect(ExpectCertificateTransparency.new(max_age: 1234, enforce: true).value).to eq("enforce; max-age=1234") }
7
+ specify { expect(ExpectCertificateTransparency.new(max_age: 1234, enforce: false).value).to eq("max-age=1234") }
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\"") }
10
+ specify do
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\""
13
+ expect(ExpectCertificateTransparency.new(config).value).to eq(header_value)
14
+ end
15
+
16
+ context "with an invalid configuration" do
17
+ it "raises an exception when configuration isn't a hash" do
18
+ expect do
19
+ ExpectCertificateTransparency.validate_config!(%w(a))
20
+ end.to raise_error(ExpectCertificateTransparencyConfigError)
21
+ end
22
+
23
+ it "raises an exception when max-age is not provided" do
24
+ expect do
25
+ ExpectCertificateTransparency.validate_config!(foo: "bar")
26
+ end.to raise_error(ExpectCertificateTransparencyConfigError)
27
+ end
28
+
29
+ it "raises an exception with an invalid max-age" do
30
+ expect do
31
+ ExpectCertificateTransparency.validate_config!(max_age: "abc123")
32
+ end.to raise_error(ExpectCertificateTransparencyConfigError)
33
+ end
34
+
35
+ it "raises an exception with an invalid enforce value" do
36
+ expect do
37
+ ExpectCertificateTransparency.validate_config!(enforce: "brokenstring")
38
+ end.to raise_error(ExpectCertificateTransparencyConfigError)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -111,6 +111,36 @@ module SecureHeaders
111
111
  ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w(self none inline eval), script_src: %w('self')))
112
112
  end.to raise_error(ContentSecurityPolicyConfigError)
113
113
  end
114
+
115
+ it "rejects anything not of the form allow-* as a sandbox value" do
116
+ expect do
117
+ ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(sandbox: ["steve"])))
118
+ end.to raise_error(ContentSecurityPolicyConfigError)
119
+ end
120
+
121
+ it "accepts anything of the form allow-* as a sandbox value " do
122
+ expect do
123
+ ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(sandbox: ["allow-foo"])))
124
+ end.to_not raise_error
125
+ end
126
+
127
+ it "accepts true as a sandbox policy" do
128
+ expect do
129
+ ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(sandbox: true)))
130
+ end.to_not raise_error
131
+ end
132
+
133
+ it "rejects anything not of the form type/subtype as a plugin-type value" do
134
+ expect do
135
+ ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(plugin_types: ["steve"])))
136
+ end.to raise_error(ContentSecurityPolicyConfigError)
137
+ end
138
+
139
+ it "accepts anything of the form type/subtype as a plugin-type value " do
140
+ expect do
141
+ ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(plugin_types: ["application/pdf"])))
142
+ end.to_not raise_error
143
+ end
114
144
  end
115
145
 
116
146
  describe "#combine_policies" do
data/spec/spec_helper.rb CHANGED
@@ -33,6 +33,7 @@ def expect_default_values(hash)
33
33
  expect(hash[SecureHeaders::XContentTypeOptions::HEADER_NAME]).to eq(SecureHeaders::XContentTypeOptions::DEFAULT_VALUE)
34
34
  expect(hash[SecureHeaders::XPermittedCrossDomainPolicies::HEADER_NAME]).to eq(SecureHeaders::XPermittedCrossDomainPolicies::DEFAULT_VALUE)
35
35
  expect(hash[SecureHeaders::ReferrerPolicy::HEADER_NAME]).to be_nil
36
+ expect(hash[SecureHeaders::ExpectCertificateTransparency::HEADER_NAME]).to be_nil
36
37
  end
37
38
 
38
39
  module SecureHeaders
data/upgrading-to-4-0.md CHANGED
@@ -18,6 +18,12 @@ config.cookies = {
18
18
  config.cookies = OPT_OUT
19
19
  ```
20
20
 
21
+ ## script_src must be set
22
+
23
+ Not setting a `script_src` value means your policy falls back to whatever `default_src` (also required) is set to. This can be very dangerous and indicates the policy is too loose.
24
+
25
+ However, sometimes you really don't need a `script-src` e.g. API responses (`default-src 'none'`) so you can set `script_src: SecureHeaders::OPT_OUT` to work around this.
26
+
21
27
  ## Default Content Security Policy
22
28
 
23
29
  The default CSP has changed to be more universal without sacrificing too much security.
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: 4.0.0.alpha03
4
+ version: 4.0.0.alpha04
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-07-25 00:00:00.000000000 Z
11
+ date: 2017-09-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -74,6 +74,7 @@ files:
74
74
  - lib/secure_headers/headers/content_security_policy.rb
75
75
  - lib/secure_headers/headers/content_security_policy_config.rb
76
76
  - lib/secure_headers/headers/cookie.rb
77
+ - lib/secure_headers/headers/expect_certificate_transparency.rb
77
78
  - lib/secure_headers/headers/policy_management.rb
78
79
  - lib/secure_headers/headers/public_key_pins.rb
79
80
  - lib/secure_headers/headers/referrer_policy.rb
@@ -93,6 +94,7 @@ files:
93
94
  - spec/lib/secure_headers/headers/clear_site_data_spec.rb
94
95
  - spec/lib/secure_headers/headers/content_security_policy_spec.rb
95
96
  - spec/lib/secure_headers/headers/cookie_spec.rb
97
+ - spec/lib/secure_headers/headers/expect_certificate_spec.rb
96
98
  - spec/lib/secure_headers/headers/policy_management_spec.rb
97
99
  - spec/lib/secure_headers/headers/public_key_pins_spec.rb
98
100
  - spec/lib/secure_headers/headers/referrer_policy_spec.rb
@@ -143,6 +145,7 @@ test_files:
143
145
  - spec/lib/secure_headers/headers/clear_site_data_spec.rb
144
146
  - spec/lib/secure_headers/headers/content_security_policy_spec.rb
145
147
  - spec/lib/secure_headers/headers/cookie_spec.rb
148
+ - spec/lib/secure_headers/headers/expect_certificate_spec.rb
146
149
  - spec/lib/secure_headers/headers/policy_management_spec.rb
147
150
  - spec/lib/secure_headers/headers/public_key_pins_spec.rb
148
151
  - spec/lib/secure_headers/headers/referrer_policy_spec.rb