secure_headers 3.6.4 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +2 -0
  3. data/.rubocop.yml +3 -0
  4. data/.ruby-version +1 -1
  5. data/.travis.yml +8 -6
  6. data/CHANGELOG.md +21 -1
  7. data/CONTRIBUTING.md +3 -5
  8. data/Gemfile +7 -4
  9. data/Guardfile +1 -0
  10. data/README.md +15 -19
  11. data/Rakefile +22 -18
  12. data/docs/cookies.md +16 -2
  13. data/docs/hashes.md +1 -1
  14. data/docs/per_action_configuration.md +28 -0
  15. data/lib/secure_headers/configuration.rb +29 -16
  16. data/lib/secure_headers/hash_helper.rb +2 -1
  17. data/lib/secure_headers/headers/clear_site_data.rb +16 -12
  18. data/lib/secure_headers/headers/content_security_policy.rb +52 -20
  19. data/lib/secure_headers/headers/content_security_policy_config.rb +25 -0
  20. data/lib/secure_headers/headers/cookie.rb +21 -3
  21. data/lib/secure_headers/headers/expect_certificate_transparency.rb +70 -0
  22. data/lib/secure_headers/headers/policy_management.rb +115 -68
  23. data/lib/secure_headers/headers/public_key_pins.rb +5 -4
  24. data/lib/secure_headers/headers/referrer_policy.rb +1 -0
  25. data/lib/secure_headers/headers/strict_transport_security.rb +2 -1
  26. data/lib/secure_headers/headers/x_content_type_options.rb +1 -0
  27. data/lib/secure_headers/headers/x_download_options.rb +2 -1
  28. data/lib/secure_headers/headers/x_frame_options.rb +1 -0
  29. data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +2 -1
  30. data/lib/secure_headers/headers/x_xss_protection.rb +2 -1
  31. data/lib/secure_headers/middleware.rb +10 -9
  32. data/lib/secure_headers/railtie.rb +7 -6
  33. data/lib/secure_headers/utils/cookies_config.rb +13 -12
  34. data/lib/secure_headers/view_helper.rb +4 -3
  35. data/lib/secure_headers.rb +6 -2
  36. data/lib/tasks/tasks.rake +2 -1
  37. data/secure_headers.gemspec +13 -3
  38. data/spec/lib/secure_headers/configuration_spec.rb +9 -8
  39. data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +11 -27
  40. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +42 -26
  41. data/spec/lib/secure_headers/headers/cookie_spec.rb +38 -20
  42. data/spec/lib/secure_headers/headers/expect_certificate_spec.rb +42 -0
  43. data/spec/lib/secure_headers/headers/policy_management_spec.rb +57 -10
  44. data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +7 -6
  45. data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +4 -3
  46. data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +5 -4
  47. data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +2 -1
  48. data/spec/lib/secure_headers/headers/x_download_options_spec.rb +3 -2
  49. data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +2 -1
  50. data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +4 -3
  51. data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +4 -3
  52. data/spec/lib/secure_headers/middleware_spec.rb +29 -21
  53. data/spec/lib/secure_headers/view_helpers_spec.rb +5 -5
  54. data/spec/lib/secure_headers_spec.rb +96 -124
  55. data/spec/spec_helper.rb +10 -22
  56. data/upgrading-to-4-0.md +55 -0
  57. metadata +16 -6
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module SecureHeaders
2
3
  module DynamicConfig
3
4
  def self.included(base)
@@ -15,6 +16,30 @@ module SecureHeaders
15
16
  end
16
17
 
17
18
  def initialize(hash)
19
+ @base_uri = nil
20
+ @block_all_mixed_content = nil
21
+ @child_src = nil
22
+ @connect_src = nil
23
+ @default_src = nil
24
+ @font_src = nil
25
+ @form_action = nil
26
+ @frame_ancestors = nil
27
+ @frame_src = nil
28
+ @img_src = nil
29
+ @manifest_src = nil
30
+ @media_src = nil
31
+ @object_src = nil
32
+ @plugin_types = nil
33
+ @preserve_schemes = nil
34
+ @report_only = nil
35
+ @report_uri = nil
36
+ @sandbox = nil
37
+ @script_nonce = nil
38
+ @script_src = nil
39
+ @style_nonce = nil
40
+ @style_src = nil
41
+ @upgrade_insecure_requests = nil
42
+
18
43
  from_hash(hash)
19
44
  @modified = false
20
45
  end
@@ -1,5 +1,7 @@
1
- require 'cgi'
2
- require 'secure_headers/utils/cookies_config'
1
+ # frozen_string_literal: true
2
+ require "cgi"
3
+ require "secure_headers/utils/cookies_config"
4
+
3
5
 
4
6
  module SecureHeaders
5
7
  class CookiesConfigError < StandardError; end
@@ -13,8 +15,18 @@ module SecureHeaders
13
15
 
14
16
  attr_reader :raw_cookie, :config
15
17
 
18
+ COOKIE_DEFAULTS = {
19
+ httponly: true,
20
+ secure: true,
21
+ samesite: { lax: true },
22
+ }.freeze
23
+
16
24
  def initialize(cookie, config)
17
25
  @raw_cookie = cookie
26
+ unless config == OPT_OUT
27
+ config ||= {}
28
+ config = COOKIE_DEFAULTS.merge(config)
29
+ end
18
30
  @config = config
19
31
  @attributes = {
20
32
  httponly: nil,
@@ -56,6 +68,7 @@ module SecureHeaders
56
68
  end
57
69
 
58
70
  def flag_cookie?(attribute)
71
+ return false if config == OPT_OUT
59
72
  case config[attribute]
60
73
  when TrueClass
61
74
  true
@@ -85,6 +98,7 @@ module SecureHeaders
85
98
  end
86
99
 
87
100
  def flag_samesite?
101
+ return false if config == OPT_OUT || config[:samesite] == OPT_OUT
88
102
  flag_samesite_lax? || flag_samesite_strict?
89
103
  end
90
104
 
@@ -99,6 +113,10 @@ module SecureHeaders
99
113
  def flag_samesite_enforcement?(mode)
100
114
  return unless config[:samesite]
101
115
 
116
+ if config[:samesite].is_a?(TrueClass) && mode == :lax
117
+ return true
118
+ end
119
+
102
120
  case config[:samesite][mode]
103
121
  when Hash
104
122
  conditionally_flag?(config[:samesite][mode])
@@ -113,7 +131,7 @@ module SecureHeaders
113
131
  return unless cookie
114
132
 
115
133
  cookie.split(/[;,]\s?/).each do |pairs|
116
- name, values = pairs.split('=',2)
134
+ name, values = pairs.split("=", 2)
117
135
  name = CGI.unescape(name)
118
136
 
119
137
  attribute = name.downcase.to_sym
@@ -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
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module SecureHeaders
2
3
  module PolicyManagement
3
4
  def self.included(base)
@@ -5,8 +6,14 @@ module SecureHeaders
5
6
  end
6
7
 
7
8
  MODERN_BROWSERS = %w(Chrome Opera Firefox)
8
- DEFAULT_VALUE = "default-src https:".freeze
9
- DEFAULT_CONFIG = { default_src: %w(https:) }.freeze
9
+ DEFAULT_CONFIG = {
10
+ default_src: %w(https:),
11
+ img_src: %w(https: data: 'self'),
12
+ object_src: %w('none'),
13
+ script_src: %w(https:),
14
+ style_src: %w('self' 'unsafe-inline' https:),
15
+ form_action: %w('self')
16
+ }.freeze
10
17
  DATA_PROTOCOL = "data:".freeze
11
18
  BLOB_PROTOCOL = "blob:".freeze
12
19
  SELF = "'self'".freeze
@@ -62,22 +69,15 @@ module SecureHeaders
62
69
 
63
70
  # All the directives currently under consideration for CSP level 3.
64
71
  # https://w3c.github.io/webappsec/specs/CSP2/
72
+ BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content
65
73
  MANIFEST_SRC = :manifest_src
66
- REFLECTED_XSS = :reflected_xss
74
+ UPGRADE_INSECURE_REQUESTS = :upgrade_insecure_requests
67
75
  DIRECTIVES_3_0 = [
68
76
  DIRECTIVES_2_0,
69
- MANIFEST_SRC,
70
- REFLECTED_XSS
71
- ].flatten.freeze
72
-
73
- # All the directives that are not currently in a formal spec, but have
74
- # been implemented somewhere.
75
- BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content
76
- UPGRADE_INSECURE_REQUESTS = :upgrade_insecure_requests
77
- DIRECTIVES_DRAFT = [
78
77
  BLOCK_ALL_MIXED_CONTENT,
78
+ MANIFEST_SRC,
79
79
  UPGRADE_INSECURE_REQUESTS
80
- ].freeze
80
+ ].flatten.freeze
81
81
 
82
82
  EDGE_DIRECTIVES = DIRECTIVES_1_0
83
83
  SAFARI_DIRECTIVES = DIRECTIVES_1_0
@@ -99,35 +99,23 @@ module SecureHeaders
99
99
  ].freeze
100
100
 
101
101
  FIREFOX_DIRECTIVES = (
102
- DIRECTIVES_2_0 + DIRECTIVES_DRAFT - FIREFOX_UNSUPPORTED_DIRECTIVES
102
+ DIRECTIVES_3_0 - FIREFOX_UNSUPPORTED_DIRECTIVES
103
103
  ).freeze
104
104
 
105
105
  FIREFOX_46_DIRECTIVES = (
106
- DIRECTIVES_2_0 + DIRECTIVES_DRAFT - FIREFOX_46_UNSUPPORTED_DIRECTIVES - FIREFOX_46_DEPRECATED_DIRECTIVES
106
+ DIRECTIVES_3_0 - FIREFOX_46_UNSUPPORTED_DIRECTIVES - FIREFOX_46_DEPRECATED_DIRECTIVES
107
107
  ).freeze
108
108
 
109
109
  CHROME_DIRECTIVES = (
110
- DIRECTIVES_2_0 + DIRECTIVES_DRAFT
110
+ DIRECTIVES_3_0
111
111
  ).freeze
112
112
 
113
- ALL_DIRECTIVES = (DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_DRAFT).uniq.sort
113
+ ALL_DIRECTIVES = (DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0).uniq.sort
114
114
 
115
115
  # Think of default-src and report-uri as the beginning and end respectively,
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,15 +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,
159
- REFLECTED_XSS => :string,
146
+ PLUGIN_TYPES => :media_type_list,
160
147
  REPORT_URI => :source_list,
161
- SANDBOX => :source_list,
148
+ SANDBOX => :sandbox_list,
162
149
  SCRIPT_SRC => :source_list,
163
150
  STYLE_SRC => :source_list,
164
151
  UPGRADE_INSECURE_REQUESTS => :boolean
165
152
  }.freeze
166
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
167
170
 
168
171
  STAR_REGEXP = Regexp.new(Regexp.escape(STAR))
169
172
  HTTP_SCHEME_REGEX = %r{\Ahttps?://}
@@ -203,6 +206,10 @@ module SecureHeaders
203
206
  def validate_config!(config)
204
207
  return if config.nil? || config.opt_out?
205
208
  raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config.directive_value(:default_src)
209
+ if config.directive_value(:script_src).nil?
210
+ raise ContentSecurityPolicyConfigError.new(":script_src is required, falling back to default-src is too dangerous. Use `script_src: OPT_OUT` to override")
211
+ end
212
+
206
213
  ContentSecurityPolicyConfig.attrs.each do |key|
207
214
  value = config.directive_value(key)
208
215
  next unless value
@@ -261,7 +268,7 @@ module SecureHeaders
261
268
  # when each hash contains a value for a given key.
262
269
  def merge_policy_additions(original, additions)
263
270
  original.merge(additions) do |directive, lhs, rhs|
264
- if source_list?(directive)
271
+ if list_directive?(directive)
265
272
  (lhs.to_a + rhs.to_a).compact.uniq
266
273
  else
267
274
  rhs
@@ -269,20 +276,27 @@ module SecureHeaders
269
276
  end.reject { |_, value| value.nil? || value == [] } # this mess prevents us from adding empty directives.
270
277
  end
271
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
+
272
286
  # For each directive in additions that does not exist in the original config,
273
287
  # copy the default-src value to the original config. This modifies the original hash.
274
288
  def populate_fetch_source_with_default!(original, additions)
275
289
  # in case we would be appending to an empty directive, fill it with the default-src value
276
290
  additions.each_key do |directive|
277
- if !original[directive] && ((source_list?(directive) && FETCH_SOURCES.include?(directive)) || nonce_added?(original, additions))
278
- if nonce_added?(original, additions)
279
- inferred_directive = directive.to_s.gsub(/_nonce/, "_src").to_sym
280
- unless original[inferred_directive] || NON_FETCH_SOURCES.include?(inferred_directive)
281
- original[inferred_directive] = default_for(directive, original)
282
- end
283
- else
284
- original[directive] = default_for(directive, original)
285
- 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)
286
300
  end
287
301
  end
288
302
  end
@@ -293,45 +307,77 @@ module SecureHeaders
293
307
  original[DEFAULT_SRC]
294
308
  end
295
309
 
296
- def nonce_added?(original, additions)
297
- [:script_nonce, :style_nonce].each do |nonce|
298
- if additions[nonce] && !original[nonce]
299
- return true
300
- end
301
- end
302
- end
303
-
304
310
  def source_list?(directive)
305
311
  DIRECTIVE_VALUE_TYPES[directive] == :source_list
306
312
  end
307
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
+
308
322
  # Private: Validates that the configuration has a valid type, or that it is a valid
309
323
  # source expression.
310
- def validate_directive!(directive, source_expression)
324
+ def validate_directive!(directive, value)
325
+ ensure_valid_directive!(directive)
311
326
  case ContentSecurityPolicy::DIRECTIVE_VALUE_TYPES[directive]
312
327
  when :boolean
313
- unless boolean?(source_expression)
314
- raise ContentSecurityPolicyConfigError.new("#{directive} must be a boolean value")
315
- end
316
- when :string
317
- unless source_expression.is_a?(String)
318
- 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")
319
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)
320
337
  else
321
- 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)")
322
368
  end
323
369
  end
324
370
 
325
371
  # Private: validates that a source expression:
326
- # 1. has a valid name
327
- # 2. is an array of strings
328
- # 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)
329
374
  #
330
375
  # Does not validate the invididual values of the source expression (e.g.
331
376
  # script_src => h*t*t*p: will not raise an exception)
332
377
  def validate_source_expression!(directive, source_expression)
333
- ensure_valid_directive!(directive)
334
- ensure_array_of_strings!(directive, source_expression)
378
+ if source_expression != OPT_OUT
379
+ ensure_array_of_strings!(directive, source_expression)
380
+ end
335
381
  ensure_valid_sources!(directive, source_expression)
336
382
  end
337
383
 
@@ -341,16 +387,17 @@ module SecureHeaders
341
387
  end
342
388
  end
343
389
 
344
- def ensure_array_of_strings!(directive, source_expression)
345
- unless source_expression.is_a?(Array) && source_expression.compact.all? { |v| v.is_a?(String) }
390
+ def ensure_array_of_strings!(directive, value)
391
+ if (!value.is_a?(Array) || !value.compact.all? { |v| v.is_a?(String) })
346
392
  raise ContentSecurityPolicyConfigError.new("#{directive} must be an array of strings")
347
393
  end
348
394
  end
349
395
 
350
396
  def ensure_valid_sources!(directive, source_expression)
351
- source_expression.each do |source_expression|
352
- if ContentSecurityPolicy::DEPRECATED_SOURCE_VALUES.include?(source_expression)
353
- raise ContentSecurityPolicyConfigError.new("#{directive} contains an invalid keyword source (#{source_expression}). This value must be single quoted.")
397
+ return if source_expression == OPT_OUT
398
+ source_expression.each do |expression|
399
+ if ContentSecurityPolicy::DEPRECATED_SOURCE_VALUES.include?(expression)
400
+ raise ContentSecurityPolicyConfigError.new("#{directive} contains an invalid keyword source (#{expression}). This value must be single quoted.")
354
401
  end
355
402
  end
356
403
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module SecureHeaders
2
3
  class PublicKeyPinsConfigError < StandardError; end
3
4
  class PublicKeyPins
@@ -49,12 +50,12 @@ module SecureHeaders
49
50
  end
50
51
 
51
52
  def value
52
- header_value = [
53
+ [
53
54
  max_age_directive,
54
55
  pin_directives,
55
56
  report_uri_directive,
56
57
  subdomain_directive
57
- ].compact.join('; ').strip
58
+ ].compact.join("; ").strip
58
59
  end
59
60
 
60
61
  def pin_directives
@@ -63,7 +64,7 @@ module SecureHeaders
63
64
  pin.map do |token, hash|
64
65
  "pin-#{token}=\"#{hash}\"" if HASH_ALGORITHMS.include?(token)
65
66
  end
66
- end.join('; ')
67
+ end.join("; ")
67
68
  end
68
69
 
69
70
  def max_age_directive
@@ -75,7 +76,7 @@ module SecureHeaders
75
76
  end
76
77
 
77
78
  def subdomain_directive
78
- @include_subdomains ? 'includeSubDomains' : nil
79
+ @include_subdomains ? "includeSubDomains" : nil
79
80
  end
80
81
  end
81
82
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module SecureHeaders
2
3
  class ReferrerPolicyConfigError < StandardError; end
3
4
  class ReferrerPolicy
@@ -1,8 +1,9 @@
1
+ # frozen_string_literal: true
1
2
  module SecureHeaders
2
3
  class STSConfigError < StandardError; end
3
4
 
4
5
  class StrictTransportSecurity
5
- HEADER_NAME = 'Strict-Transport-Security'.freeze
6
+ HEADER_NAME = "Strict-Transport-Security".freeze
6
7
  HSTS_MAX_AGE = "631138519"
7
8
  DEFAULT_VALUE = "max-age=" + HSTS_MAX_AGE
8
9
  VALID_STS_HEADER = /\Amax-age=\d+(; includeSubdomains)?(; preload)?\z/i
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module SecureHeaders
2
3
  class XContentTypeOptionsConfigError < StandardError; end
3
4
 
@@ -1,8 +1,9 @@
1
+ # frozen_string_literal: true
1
2
  module SecureHeaders
2
3
  class XDOConfigError < StandardError; end
3
4
  class XDownloadOptions
4
5
  HEADER_NAME = "X-Download-Options".freeze
5
- DEFAULT_VALUE = 'noopen'
6
+ DEFAULT_VALUE = "noopen"
6
7
  CONFIG_KEY = :x_download_options
7
8
 
8
9
  class << self
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module SecureHeaders
2
3
  class XFOConfigError < StandardError; end
3
4
  class XFrameOptions
@@ -1,8 +1,9 @@
1
+ # frozen_string_literal: true
1
2
  module SecureHeaders
2
3
  class XPCDPConfigError < StandardError; end
3
4
  class XPermittedCrossDomainPolicies
4
5
  HEADER_NAME = "X-Permitted-Cross-Domain-Policies".freeze
5
- DEFAULT_VALUE = 'none'
6
+ DEFAULT_VALUE = "none"
6
7
  VALID_POLICIES = %w(all none master-only by-content-type by-ftp-filename)
7
8
  CONFIG_KEY = :x_permitted_cross_domain_policies
8
9
 
@@ -1,7 +1,8 @@
1
+ # frozen_string_literal: true
1
2
  module SecureHeaders
2
3
  class XXssProtectionConfigError < StandardError; end
3
4
  class XXssProtection
4
- HEADER_NAME = 'X-XSS-Protection'.freeze
5
+ HEADER_NAME = "X-XSS-Protection".freeze
5
6
  DEFAULT_VALUE = "1; mode=block"
6
7
  VALID_X_XSS_HEADER = /\A[01](; mode=block)?(; report=.*)?\z/i
7
8
  CONFIG_KEY = :x_xss_protection
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module SecureHeaders
2
3
  class Middleware
3
4
  HPKP_SAME_HOST_WARNING = "[WARNING] HPKP report host should not be the same as the request host. See https://github.com/twitter/secureheaders/issues/166"
@@ -25,11 +26,11 @@ module SecureHeaders
25
26
 
26
27
  # inspired by https://github.com/tobmatth/rack-ssl-enforcer/blob/6c014/lib/rack/ssl-enforcer.rb#L183-L194
27
28
  def flag_cookies!(headers, config)
28
- if cookies = headers['Set-Cookie']
29
+ if cookies = headers["Set-Cookie"]
29
30
  # Support Rails 2.3 / Rack 1.1 arrays as headers
30
31
  cookies = cookies.split("\n") unless cookies.is_a?(Array)
31
32
 
32
- headers['Set-Cookie'] = cookies.map do |cookie|
33
+ headers["Set-Cookie"] = cookies.map do |cookie|
33
34
  SecureHeaders::Cookie.new(cookie, config).to_s
34
35
  end.join("\n")
35
36
  end
@@ -37,8 +38,8 @@ module SecureHeaders
37
38
 
38
39
  # disable Secure cookies for non-https requests
39
40
  def override_secure(env, config = {})
40
- if scheme(env) != 'https'
41
- config.merge!(secure: false)
41
+ if scheme(env) != "https" && config != OPT_OUT
42
+ config[:secure] = OPT_OUT
42
43
  end
43
44
 
44
45
  config
@@ -46,12 +47,12 @@ module SecureHeaders
46
47
 
47
48
  # derived from https://github.com/tobmatth/rack-ssl-enforcer/blob/6c014/lib/rack/ssl-enforcer.rb#L119
48
49
  def scheme(env)
49
- if env['HTTPS'] == 'on' || env['HTTP_X_SSL_REQUEST'] == 'on'
50
- 'https'
51
- elsif env['HTTP_X_FORWARDED_PROTO']
52
- env['HTTP_X_FORWARDED_PROTO'].split(',')[0]
50
+ if env["HTTPS"] == "on" || env["HTTP_X_SSL_REQUEST"] == "on"
51
+ "https"
52
+ elsif env["HTTP_X_FORWARDED_PROTO"]
53
+ env["HTTP_X_FORWARDED_PROTO"].split(",")[0]
53
54
  else
54
- env['rack.url_scheme']
55
+ env["rack.url_scheme"]
55
56
  end
56
57
  end
57
58
  end
@@ -1,20 +1,21 @@
1
+ # frozen_string_literal: true
1
2
  # rails 3.1+
2
3
  if defined?(Rails::Railtie)
3
4
  module SecureHeaders
4
5
  class Railtie < Rails::Railtie
5
6
  isolate_namespace SecureHeaders if defined? isolate_namespace # rails 3.0
6
- conflicting_headers = ['X-Frame-Options', 'X-XSS-Protection',
7
- 'X-Permitted-Cross-Domain-Policies', 'X-Download-Options',
8
- 'X-Content-Type-Options', 'Strict-Transport-Security',
9
- 'Content-Security-Policy', 'Content-Security-Policy-Report-Only',
10
- 'Public-Key-Pins', 'Public-Key-Pins-Report-Only', 'Referrer-Policy']
7
+ conflicting_headers = ["X-Frame-Options", "X-XSS-Protection",
8
+ "X-Permitted-Cross-Domain-Policies", "X-Download-Options",
9
+ "X-Content-Type-Options", "Strict-Transport-Security",
10
+ "Content-Security-Policy", "Content-Security-Policy-Report-Only",
11
+ "Public-Key-Pins", "Public-Key-Pins-Report-Only", "Referrer-Policy"]
11
12
 
12
13
  initializer "secure_headers.middleware" do
13
14
  Rails.application.config.middleware.insert_before 0, SecureHeaders::Middleware
14
15
  end
15
16
 
16
17
  rake_tasks do
17
- load File.expand_path(File.join('..', '..', 'lib', 'tasks', 'tasks.rake'), File.dirname(__FILE__))
18
+ load File.expand_path(File.join("..", "..", "lib", "tasks", "tasks.rake"), File.dirname(__FILE__))
18
19
  end
19
20
 
20
21
  initializer "secure_headers.action_controller" do