secure_headers 6.3.4 → 7.3.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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -2
  3. data/Gemfile +2 -0
  4. data/README.md +81 -15
  5. data/lib/secure_headers/configuration.rb +64 -13
  6. data/lib/secure_headers/headers/clear_site_data.rb +31 -33
  7. data/lib/secure_headers/headers/content_security_policy.rb +80 -46
  8. data/lib/secure_headers/headers/content_security_policy_config.rb +17 -58
  9. data/lib/secure_headers/headers/cookie.rb +4 -6
  10. data/lib/secure_headers/headers/expect_certificate_transparency.rb +20 -22
  11. data/lib/secure_headers/headers/policy_management.rb +66 -18
  12. data/lib/secure_headers/headers/referrer_policy.rb +20 -22
  13. data/lib/secure_headers/headers/reporting_endpoints.rb +54 -0
  14. data/lib/secure_headers/headers/strict_transport_security.rb +13 -15
  15. data/lib/secure_headers/headers/x_content_type_options.rb +14 -16
  16. data/lib/secure_headers/headers/x_download_options.rb +14 -16
  17. data/lib/secure_headers/headers/x_frame_options.rb +14 -16
  18. data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +14 -16
  19. data/lib/secure_headers/headers/x_xss_protection.rb +14 -16
  20. data/lib/secure_headers/middleware.rb +11 -7
  21. data/lib/secure_headers/railtie.rb +11 -8
  22. data/lib/secure_headers/task_helper.rb +65 -0
  23. data/lib/secure_headers/version.rb +1 -1
  24. data/lib/secure_headers/view_helper.rb +7 -6
  25. data/lib/secure_headers.rb +26 -2
  26. data/lib/tasks/tasks.rake +4 -53
  27. data/secure_headers.gemspec +19 -7
  28. metadata +38 -70
  29. data/.github/ISSUE_TEMPLATE.md +0 -41
  30. data/.github/PULL_REQUEST_TEMPLATE.md +0 -20
  31. data/.github/workflows/build.yml +0 -24
  32. data/.github/workflows/github-release.yml +0 -28
  33. data/.gitignore +0 -13
  34. data/.rspec +0 -3
  35. data/.rubocop.yml +0 -4
  36. data/.ruby-gemset +0 -1
  37. data/.ruby-version +0 -1
  38. data/CODE_OF_CONDUCT.md +0 -46
  39. data/CONTRIBUTING.md +0 -41
  40. data/Guardfile +0 -13
  41. data/Rakefile +0 -32
  42. data/docs/cookies.md +0 -65
  43. data/docs/hashes.md +0 -64
  44. data/docs/named_overrides_and_appends.md +0 -104
  45. data/docs/per_action_configuration.md +0 -139
  46. data/docs/sinatra.md +0 -25
  47. data/docs/upgrading-to-3-0.md +0 -42
  48. data/docs/upgrading-to-4-0.md +0 -35
  49. data/docs/upgrading-to-5-0.md +0 -15
  50. data/docs/upgrading-to-6-0.md +0 -50
  51. data/spec/lib/secure_headers/configuration_spec.rb +0 -121
  52. data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +0 -87
  53. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +0 -190
  54. data/spec/lib/secure_headers/headers/cookie_spec.rb +0 -179
  55. data/spec/lib/secure_headers/headers/expect_certificate_transparency_spec.rb +0 -42
  56. data/spec/lib/secure_headers/headers/policy_management_spec.rb +0 -264
  57. data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +0 -91
  58. data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +0 -33
  59. data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +0 -31
  60. data/spec/lib/secure_headers/headers/x_download_options_spec.rb +0 -29
  61. data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +0 -36
  62. data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +0 -48
  63. data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +0 -47
  64. data/spec/lib/secure_headers/middleware_spec.rb +0 -117
  65. data/spec/lib/secure_headers/view_helpers_spec.rb +0 -191
  66. data/spec/lib/secure_headers_spec.rb +0 -516
  67. data/spec/spec_helper.rb +0 -64
@@ -1,64 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
  module SecureHeaders
3
3
  module DynamicConfig
4
- def self.included(base)
5
- base.send(:attr_reader, *base.attrs)
6
- base.attrs.each do |attr|
7
- base.send(:define_method, "#{attr}=") do |value|
8
- if self.class.attrs.include?(attr)
9
- write_attribute(attr, value)
10
- else
11
- raise ContentSecurityPolicyConfigError, "Unknown config directive: #{attr}=#{value}"
12
- end
13
- end
14
- end
15
- end
16
-
17
4
  def initialize(hash)
18
- @base_uri = nil
19
- @block_all_mixed_content = nil
20
- @child_src = nil
21
- @connect_src = nil
22
- @default_src = nil
23
- @font_src = nil
24
- @form_action = nil
25
- @frame_ancestors = nil
26
- @frame_src = nil
27
- @img_src = nil
28
- @manifest_src = nil
29
- @media_src = nil
30
- @navigate_to = nil
31
- @object_src = nil
32
- @plugin_types = nil
33
- @prefetch_src = nil
34
- @preserve_schemes = nil
35
- @report_only = nil
36
- @report_uri = nil
37
- @require_sri_for = nil
38
- @sandbox = nil
39
- @script_nonce = nil
40
- @script_src = nil
41
- @script_src_elem = nil
42
- @script_src_attr = nil
43
- @style_nonce = nil
44
- @style_src = nil
45
- @style_src_elem = nil
46
- @style_src_attr = nil
47
- @worker_src = nil
48
- @upgrade_insecure_requests = nil
49
- @disable_nonce_backwards_compatibility = nil
5
+ @config = {}
50
6
 
51
7
  from_hash(hash)
52
8
  end
53
9
 
10
+ def initialize_copy(hash)
11
+ @config = hash.to_h
12
+ end
13
+
54
14
  def update_directive(directive, value)
55
- self.send("#{directive}=", value)
15
+ @config[directive] = value
56
16
  end
57
17
 
58
18
  def directive_value(directive)
59
- if self.class.attrs.include?(directive)
60
- self.send(directive)
61
- end
19
+ # No need to check attrs, as we only assign valid keys
20
+ @config[directive]
62
21
  end
63
22
 
64
23
  def merge(new_hash)
@@ -76,10 +35,7 @@ module SecureHeaders
76
35
  end
77
36
 
78
37
  def to_h
79
- self.class.attrs.each_with_object({}) do |key, hash|
80
- value = self.send(key)
81
- hash[key] = value unless value.nil?
82
- end
38
+ @config.dup
83
39
  end
84
40
 
85
41
  def dup
@@ -112,16 +68,19 @@ module SecureHeaders
112
68
 
113
69
  def write_attribute(attr, value)
114
70
  value = value.dup if PolicyManagement::DIRECTIVE_VALUE_TYPES[attr] == :source_list
115
- attr_variable = "@#{attr}"
116
- self.instance_variable_set(attr_variable, value)
71
+ if value.nil?
72
+ @config.delete(attr)
73
+ else
74
+ @config[attr] = value
75
+ end
117
76
  end
118
77
  end
119
78
 
120
79
  class ContentSecurityPolicyConfigError < StandardError; end
121
80
  class ContentSecurityPolicyConfig
122
- HEADER_NAME = "Content-Security-Policy".freeze
81
+ HEADER_NAME = "content-security-policy".freeze
123
82
 
124
- ATTRS = PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES
83
+ ATTRS = Set.new(PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES)
125
84
  def self.attrs
126
85
  ATTRS
127
86
  end
@@ -148,7 +107,7 @@ module SecureHeaders
148
107
  end
149
108
 
150
109
  class ContentSecurityPolicyReportOnlyConfig < ContentSecurityPolicyConfig
151
- HEADER_NAME = "Content-Security-Policy-Report-Only".freeze
110
+ HEADER_NAME = "content-security-policy-report-only".freeze
152
111
 
153
112
  def report_only?
154
113
  true
@@ -7,10 +7,8 @@ module SecureHeaders
7
7
  class CookiesConfigError < StandardError; end
8
8
  class Cookie
9
9
 
10
- class << self
11
- def validate_config!(config)
12
- CookiesConfig.new(config).validate!
13
- end
10
+ def self.validate_config!(config)
11
+ CookiesConfig.new(config).validate!
14
12
  end
15
13
 
16
14
  attr_reader :raw_cookie, :config
@@ -80,9 +78,9 @@ module SecureHeaders
80
78
  end
81
79
 
82
80
  def conditionally_flag?(configuration)
83
- if(Array(configuration[:only]).any? && (Array(configuration[:only]) & parsed_cookie.keys).any?)
81
+ if (Array(configuration[:only]).any? && (Array(configuration[:only]) & parsed_cookie.keys).any?)
84
82
  true
85
- elsif(Array(configuration[:except]).any? && (Array(configuration[:except]) & parsed_cookie.keys).none?)
83
+ elsif (Array(configuration[:except]).any? && (Array(configuration[:except]) & parsed_cookie.keys).none?)
86
84
  true
87
85
  else
88
86
  false
@@ -3,37 +3,35 @@ module SecureHeaders
3
3
  class ExpectCertificateTransparencyConfigError < StandardError; end
4
4
 
5
5
  class ExpectCertificateTransparency
6
- HEADER_NAME = "Expect-CT".freeze
6
+ HEADER_NAME = "expect-ct".freeze
7
7
  INVALID_CONFIGURATION_ERROR = "config must be a hash.".freeze
8
8
  INVALID_ENFORCE_VALUE_ERROR = "enforce must be a boolean".freeze
9
9
  REQUIRED_MAX_AGE_ERROR = "max-age is a required directive.".freeze
10
10
  INVALID_MAX_AGE_ERROR = "max-age must be a number.".freeze
11
11
 
12
- class << self
13
- # Public: Generate a Expect-CT header.
14
- #
15
- # Returns nil if not configured, returns header name and value if
16
- # configured.
17
- def make_header(config, use_agent = nil)
18
- return if config.nil? || config == OPT_OUT
12
+ # Public: Generate a expect-ct header.
13
+ #
14
+ # Returns nil if not configured, returns header name and value if
15
+ # configured.
16
+ def self.make_header(config, use_agent = nil)
17
+ return if config.nil? || config == OPT_OUT
19
18
 
20
- header = new(config)
21
- [HEADER_NAME, header.value]
22
- end
19
+ header = new(config)
20
+ [HEADER_NAME, header.value]
21
+ end
23
22
 
24
- def validate_config!(config)
25
- return if config.nil? || config == OPT_OUT
26
- raise ExpectCertificateTransparencyConfigError.new(INVALID_CONFIGURATION_ERROR) unless config.is_a? Hash
23
+ def self.validate_config!(config)
24
+ return if config.nil? || config == OPT_OUT
25
+ raise ExpectCertificateTransparencyConfigError.new(INVALID_CONFIGURATION_ERROR) unless config.is_a? Hash
27
26
 
28
- unless [true, false, nil].include?(config[:enforce])
29
- raise ExpectCertificateTransparencyConfigError.new(INVALID_ENFORCE_VALUE_ERROR)
30
- end
27
+ unless [true, false, nil].include?(config[:enforce])
28
+ raise ExpectCertificateTransparencyConfigError.new(INVALID_ENFORCE_VALUE_ERROR)
29
+ end
31
30
 
32
- if !config[:max_age]
33
- raise ExpectCertificateTransparencyConfigError.new(REQUIRED_MAX_AGE_ERROR)
34
- elsif config[:max_age].to_s !~ /\A\d+\z/
35
- raise ExpectCertificateTransparencyConfigError.new(INVALID_MAX_AGE_ERROR)
36
- end
31
+ if !config[:max_age]
32
+ raise ExpectCertificateTransparencyConfigError.new(REQUIRED_MAX_AGE_ERROR)
33
+ elsif config[:max_age].to_s !~ /\A\d+\z/
34
+ raise ExpectCertificateTransparencyConfigError.new(INVALID_MAX_AGE_ERROR)
37
35
  end
38
36
  end
39
37
 
@@ -39,6 +39,7 @@ module SecureHeaders
39
39
  SCRIPT_SRC = :script_src
40
40
  STYLE_SRC = :style_src
41
41
  REPORT_URI = :report_uri
42
+ REPORT_TO = :report_to
42
43
 
43
44
  DIRECTIVES_1_0 = [
44
45
  DEFAULT_SRC,
@@ -51,7 +52,8 @@ module SecureHeaders
51
52
  SANDBOX,
52
53
  SCRIPT_SRC,
53
54
  STYLE_SRC,
54
- REPORT_URI
55
+ REPORT_URI,
56
+ REPORT_TO
55
57
  ].freeze
56
58
 
57
59
  BASE_URI = :base_uri
@@ -71,7 +73,6 @@ module SecureHeaders
71
73
 
72
74
  # All the directives currently under consideration for CSP level 3.
73
75
  # https://w3c.github.io/webappsec/specs/CSP2/
74
- BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content
75
76
  MANIFEST_SRC = :manifest_src
76
77
  NAVIGATE_TO = :navigate_to
77
78
  PREFETCH_SRC = :prefetch_src
@@ -85,7 +86,6 @@ module SecureHeaders
85
86
 
86
87
  DIRECTIVES_3_0 = [
87
88
  DIRECTIVES_2_0,
88
- BLOCK_ALL_MIXED_CONTENT,
89
89
  MANIFEST_SRC,
90
90
  NAVIGATE_TO,
91
91
  PREFETCH_SRC,
@@ -98,15 +98,26 @@ module SecureHeaders
98
98
  STYLE_SRC_ATTR
99
99
  ].flatten.freeze
100
100
 
101
- ALL_DIRECTIVES = (DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0).uniq.sort
101
+ # Experimental directives - these vary greatly in support
102
+ # See MDN for details.
103
+ # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/content-security-policy/trusted-types
104
+ TRUSTED_TYPES = :trusted_types
105
+ # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/content-security-policy/require-trusted-types-for
106
+ REQUIRE_TRUSTED_TYPES_FOR = :require_trusted_types_for
107
+
108
+ DIRECTIVES_EXPERIMENTAL = [
109
+ TRUSTED_TYPES,
110
+ REQUIRE_TRUSTED_TYPES_FOR,
111
+ ].flatten.freeze
102
112
 
103
- # Think of default-src and report-uri as the beginning and end respectively,
113
+ ALL_DIRECTIVES = (DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_EXPERIMENTAL).uniq.sort
114
+
115
+ # Think of default-src and report-uri/report-to as the beginning and end respectively,
104
116
  # everything else is in between.
105
- BODY_DIRECTIVES = ALL_DIRECTIVES - [DEFAULT_SRC, REPORT_URI]
117
+ BODY_DIRECTIVES = ALL_DIRECTIVES - [DEFAULT_SRC, REPORT_URI, REPORT_TO]
106
118
 
107
119
  DIRECTIVE_VALUE_TYPES = {
108
120
  BASE_URI => :source_list,
109
- BLOCK_ALL_MIXED_CONTENT => :boolean,
110
121
  CHILD_SRC => :source_list,
111
122
  CONNECT_SRC => :source_list,
112
123
  DEFAULT_SRC => :source_list,
@@ -120,9 +131,11 @@ module SecureHeaders
120
131
  NAVIGATE_TO => :source_list,
121
132
  OBJECT_SRC => :source_list,
122
133
  PLUGIN_TYPES => :media_type_list,
123
- REQUIRE_SRI_FOR => :require_sri_for_list,
124
- REPORT_URI => :source_list,
125
134
  PREFETCH_SRC => :source_list,
135
+ REPORT_TO => :report_to_endpoint,
136
+ REPORT_URI => :source_list,
137
+ REQUIRE_SRI_FOR => :require_sri_for_list,
138
+ REQUIRE_TRUSTED_TYPES_FOR => :require_trusted_types_for_list,
126
139
  SANDBOX => :sandbox_list,
127
140
  SCRIPT_SRC => :source_list,
128
141
  SCRIPT_SRC_ELEM => :source_list,
@@ -130,6 +143,7 @@ module SecureHeaders
130
143
  STYLE_SRC => :source_list,
131
144
  STYLE_SRC_ELEM => :source_list,
132
145
  STYLE_SRC_ATTR => :source_list,
146
+ TRUSTED_TYPES => :source_list,
133
147
  WORKER_SRC => :source_list,
134
148
  UPGRADE_INSECURE_REQUESTS => :boolean,
135
149
  }.freeze
@@ -147,6 +161,7 @@ module SecureHeaders
147
161
  FORM_ACTION,
148
162
  FRAME_ANCESTORS,
149
163
  NAVIGATE_TO,
164
+ REPORT_TO,
150
165
  REPORT_URI,
151
166
  ]
152
167
 
@@ -175,6 +190,7 @@ module SecureHeaders
175
190
  ].freeze
176
191
 
177
192
  REQUIRE_SRI_FOR_VALUES = Set.new(%w(script style))
193
+ REQUIRE_TRUSTED_TYPES_FOR_VALUES = Set.new(%w('script'))
178
194
 
179
195
  module ClassMethods
180
196
  # Public: generate a header name, value array that is user-agent-aware.
@@ -189,7 +205,7 @@ module SecureHeaders
189
205
 
190
206
  # Public: Validates each source expression.
191
207
  #
192
- # Does not validate the invididual values of the source expression (e.g.
208
+ # Does not validate the individual values of the source expression (e.g.
193
209
  # script_src => h*t*t*p: will not raise an exception)
194
210
  def validate_config!(config)
195
211
  return if config.nil? || config.opt_out?
@@ -226,7 +242,7 @@ module SecureHeaders
226
242
  #
227
243
  # raises an error if the original config is OPT_OUT
228
244
  #
229
- # 1. for non-source-list values (report_only, block_all_mixed_content, upgrade_insecure_requests),
245
+ # 1. for non-source-list values (report_only, upgrade_insecure_requests),
230
246
  # additions will overwrite the original value.
231
247
  # 2. if a value in additions does not exist in the original config, the
232
248
  # default-src value is included to match original behavior.
@@ -270,7 +286,8 @@ module SecureHeaders
270
286
  source_list?(directive) ||
271
287
  sandbox_list?(directive) ||
272
288
  media_type_list?(directive) ||
273
- require_sri_for_list?(directive)
289
+ require_sri_for_list?(directive) ||
290
+ require_trusted_types_for_list?(directive)
274
291
  end
275
292
 
276
293
  # For each directive in additions that does not exist in the original config,
@@ -278,11 +295,12 @@ module SecureHeaders
278
295
  def populate_fetch_source_with_default!(original, additions)
279
296
  # in case we would be appending to an empty directive, fill it with the default-src value
280
297
  additions.each_key do |directive|
281
- directive = if directive.to_s.end_with?("_nonce")
282
- directive.to_s.gsub(/_nonce/, "_src").to_sym
283
- else
284
- directive
285
- end
298
+ directive =
299
+ if directive.to_s.end_with?("_nonce")
300
+ directive.to_s.gsub(/_nonce/, "_src").to_sym
301
+ else
302
+ directive
303
+ end
286
304
  # Don't set a default if directive has an existing value
287
305
  next if original[directive]
288
306
  if FETCH_SOURCES.include?(directive)
@@ -307,6 +325,10 @@ module SecureHeaders
307
325
  DIRECTIVE_VALUE_TYPES[directive] == :require_sri_for_list
308
326
  end
309
327
 
328
+ def require_trusted_types_for_list?(directive)
329
+ DIRECTIVE_VALUE_TYPES[directive] == :require_trusted_types_for_list
330
+ end
331
+
310
332
  # Private: Validates that the configuration has a valid type, or that it is a valid
311
333
  # source expression.
312
334
  def validate_directive!(directive, value)
@@ -324,6 +346,10 @@ module SecureHeaders
324
346
  validate_media_type_expression!(directive, value)
325
347
  when :require_sri_for_list
326
348
  validate_require_sri_source_expression!(directive, value)
349
+ when :require_trusted_types_for_list
350
+ validate_require_trusted_types_for_source_expression!(directive, value)
351
+ when :report_to_endpoint
352
+ validate_report_to_endpoint_expression!(directive, value)
327
353
  else
328
354
  raise ContentSecurityPolicyConfigError.new("Unknown directive #{directive}")
329
355
  end
@@ -368,11 +394,33 @@ module SecureHeaders
368
394
  end
369
395
  end
370
396
 
397
+ # Private: validates that a require trusted types for expression:
398
+ # 1. is an array of strings
399
+ # 2. is a subset of ["'script'"]
400
+ def validate_require_trusted_types_for_source_expression!(directive, require_trusted_types_for_expression)
401
+ ensure_array_of_strings!(directive, require_trusted_types_for_expression)
402
+ unless require_trusted_types_for_expression.to_set.subset?(REQUIRE_TRUSTED_TYPES_FOR_VALUES)
403
+ raise ContentSecurityPolicyConfigError.new(%(require-trusted-types-for for must be a subset of #{REQUIRE_TRUSTED_TYPES_FOR_VALUES.to_a} but was #{require_trusted_types_for_expression}))
404
+ end
405
+ end
406
+
407
+ # Private: validates that a report-to endpoint expression:
408
+ # 1. is a string
409
+ # 2. is not empty
410
+ def validate_report_to_endpoint_expression!(directive, endpoint_name)
411
+ unless endpoint_name.is_a?(String)
412
+ raise ContentSecurityPolicyConfigError.new("#{directive} must be a string. Found #{endpoint_name.class} value")
413
+ end
414
+ if endpoint_name.empty?
415
+ raise ContentSecurityPolicyConfigError.new("#{directive} must not be empty")
416
+ end
417
+ end
418
+
371
419
  # Private: validates that a source expression:
372
420
  # 1. is an array of strings
373
421
  # 2. does not contain any deprecated, now invalid values (inline, eval, self, none)
374
422
  #
375
- # Does not validate the invididual values of the source expression (e.g.
423
+ # Does not validate the individual values of the source expression (e.g.
376
424
  # script_src => h*t*t*p: will not raise an exception)
377
425
  def validate_source_expression!(directive, source_expression)
378
426
  if source_expression != OPT_OUT
@@ -2,7 +2,7 @@
2
2
  module SecureHeaders
3
3
  class ReferrerPolicyConfigError < StandardError; end
4
4
  class ReferrerPolicy
5
- HEADER_NAME = "Referrer-Policy".freeze
5
+ HEADER_NAME = "referrer-policy".freeze
6
6
  DEFAULT_VALUE = "origin-when-cross-origin"
7
7
  VALID_POLICIES = %w(
8
8
  no-referrer
@@ -15,29 +15,27 @@ module SecureHeaders
15
15
  unsafe-url
16
16
  )
17
17
 
18
- class << self
19
- # Public: generate an Referrer Policy header.
20
- #
21
- # Returns a default header if no configuration is provided, or a
22
- # header name and value based on the config.
23
- def make_header(config = nil, user_agent = nil)
24
- return if config == OPT_OUT
25
- config ||= DEFAULT_VALUE
26
- [HEADER_NAME, Array(config).join(", ")]
27
- end
18
+ # Public: generate an Referrer Policy header.
19
+ #
20
+ # Returns a default header if no configuration is provided, or a
21
+ # header name and value based on the config.
22
+ def self.make_header(config = nil, user_agent = nil)
23
+ return if config == OPT_OUT
24
+ config ||= DEFAULT_VALUE
25
+ [HEADER_NAME, Array(config).join(", ")]
26
+ end
28
27
 
29
- def validate_config!(config)
30
- case config
31
- when nil, OPT_OUT
32
- # valid
33
- when String, Array
34
- config = Array(config)
35
- unless config.all? { |t| t.is_a?(String) && VALID_POLICIES.include?(t.downcase) }
36
- raise ReferrerPolicyConfigError.new("Value can only be one or more of #{VALID_POLICIES.join(", ")}")
37
- end
38
- else
39
- raise TypeError.new("Must be a string or array of strings. Found #{config.class}: #{config}")
28
+ def self.validate_config!(config)
29
+ case config
30
+ when nil, OPT_OUT
31
+ # valid
32
+ when String, Array
33
+ config = Array(config)
34
+ unless config.all? { |t| t.is_a?(String) && VALID_POLICIES.include?(t.downcase) }
35
+ raise ReferrerPolicyConfigError.new("Value can only be one or more of #{VALID_POLICIES.join(", ")}")
40
36
  end
37
+ else
38
+ raise TypeError.new("Must be a string or array of strings. Found #{config.class}: #{config}")
41
39
  end
42
40
  end
43
41
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+ module SecureHeaders
3
+ class ReportingEndpointsConfigError < StandardError; end
4
+ class ReportingEndpoints
5
+ HEADER_NAME = "reporting-endpoints".freeze
6
+
7
+ class << self
8
+ # Public: generate a Reporting-Endpoints header.
9
+ #
10
+ # The config should be a Hash of endpoint names to URLs.
11
+ # Example: { "csp-endpoint" => "https://example.com/reports" }
12
+ #
13
+ # Returns nil if config is OPT_OUT or nil, or a header name and
14
+ # formatted header value based on the config.
15
+ def make_header(config = nil)
16
+ return if config.nil? || config == OPT_OUT
17
+ validate_config!(config)
18
+ [HEADER_NAME, format_endpoints(config)]
19
+ end
20
+
21
+ def validate_config!(config)
22
+ case config
23
+ when nil, OPT_OUT
24
+ # valid
25
+ when Hash
26
+ config.each_pair do |name, url|
27
+ if name.is_a?(Symbol)
28
+ name = name.to_s
29
+ end
30
+ unless name.is_a?(String) && !name.empty?
31
+ raise ReportingEndpointsConfigError.new("Endpoint name must be a non-empty string, got: #{name.inspect}")
32
+ end
33
+ unless url.is_a?(String) && !url.empty?
34
+ raise ReportingEndpointsConfigError.new("Endpoint URL must be a non-empty string, got: #{url.inspect}")
35
+ end
36
+ unless url.start_with?("https://")
37
+ raise ReportingEndpointsConfigError.new("Endpoint URLs must use https, got: #{url.inspect}")
38
+ end
39
+ end
40
+ else
41
+ raise TypeError.new("Must be a Hash of endpoint names to URLs. Found #{config.class}: #{config}")
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def format_endpoints(config)
48
+ config.map do |name, url|
49
+ %{#{name}="#{url}"}
50
+ end.join(", ")
51
+ end
52
+ end
53
+ end
54
+ end
@@ -3,27 +3,25 @@ module SecureHeaders
3
3
  class STSConfigError < StandardError; end
4
4
 
5
5
  class StrictTransportSecurity
6
- HEADER_NAME = "Strict-Transport-Security".freeze
6
+ HEADER_NAME = "strict-transport-security".freeze
7
7
  HSTS_MAX_AGE = "631138519"
8
8
  DEFAULT_VALUE = "max-age=" + HSTS_MAX_AGE
9
9
  VALID_STS_HEADER = /\Amax-age=\d+(; includeSubdomains)?(; preload)?\z/i
10
10
  MESSAGE = "The config value supplied for the HSTS header was invalid. Must match #{VALID_STS_HEADER}"
11
11
 
12
- class << self
13
- # Public: generate an hsts header name, value pair.
14
- #
15
- # Returns a default header if no configuration is provided, or a
16
- # header name and value based on the config.
17
- def make_header(config = nil, user_agent = nil)
18
- return if config == OPT_OUT
19
- [HEADER_NAME, config || DEFAULT_VALUE]
20
- end
12
+ # Public: generate an hsts header name, value pair.
13
+ #
14
+ # Returns a default header if no configuration is provided, or a
15
+ # header name and value based on the config.
16
+ def self.make_header(config = nil, user_agent = nil)
17
+ return if config == OPT_OUT
18
+ [HEADER_NAME, config || DEFAULT_VALUE]
19
+ end
21
20
 
22
- def validate_config!(config)
23
- return if config.nil? || config == OPT_OUT
24
- raise TypeError.new("Must be a string. Found #{config.class}: #{config} #{config.class}") unless config.is_a?(String)
25
- raise STSConfigError.new(MESSAGE) unless config =~ VALID_STS_HEADER
26
- end
21
+ def self.validate_config!(config)
22
+ return if config.nil? || config == OPT_OUT
23
+ raise TypeError.new("Must be a string. Found #{config.class}: #{config} #{config.class}") unless config.is_a?(String)
24
+ raise STSConfigError.new(MESSAGE) unless config =~ VALID_STS_HEADER
27
25
  end
28
26
  end
29
27
  end
@@ -3,25 +3,23 @@ module SecureHeaders
3
3
  class XContentTypeOptionsConfigError < StandardError; end
4
4
 
5
5
  class XContentTypeOptions
6
- HEADER_NAME = "X-Content-Type-Options".freeze
6
+ HEADER_NAME = "x-content-type-options".freeze
7
7
  DEFAULT_VALUE = "nosniff"
8
8
 
9
- class << self
10
- # Public: generate an X-Content-Type-Options header.
11
- #
12
- # Returns a default header if no configuration is provided, or a
13
- # header name and value based on the config.
14
- def make_header(config = nil, user_agent = nil)
15
- return if config == OPT_OUT
16
- [HEADER_NAME, config || DEFAULT_VALUE]
17
- end
9
+ # Public: generate an X-Content-Type-Options header.
10
+ #
11
+ # Returns a default header if no configuration is provided, or a
12
+ # header name and value based on the config.
13
+ def self.make_header(config = nil, user_agent = nil)
14
+ return if config == OPT_OUT
15
+ [HEADER_NAME, config || DEFAULT_VALUE]
16
+ end
18
17
 
19
- def validate_config!(config)
20
- return if config.nil? || config == OPT_OUT
21
- raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String)
22
- unless config.casecmp(DEFAULT_VALUE) == 0
23
- raise XContentTypeOptionsConfigError.new("Value can only be nil or 'nosniff'")
24
- end
18
+ def self.validate_config!(config)
19
+ return if config.nil? || config == OPT_OUT
20
+ raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String)
21
+ unless config.casecmp(DEFAULT_VALUE) == 0
22
+ raise XContentTypeOptionsConfigError.new("Value can only be nil or 'nosniff'")
25
23
  end
26
24
  end
27
25
  end
@@ -2,25 +2,23 @@
2
2
  module SecureHeaders
3
3
  class XDOConfigError < StandardError; end
4
4
  class XDownloadOptions
5
- HEADER_NAME = "X-Download-Options".freeze
5
+ HEADER_NAME = "x-download-options".freeze
6
6
  DEFAULT_VALUE = "noopen"
7
7
 
8
- class << self
9
- # Public: generate an X-Download-Options header.
10
- #
11
- # Returns a default header if no configuration is provided, or a
12
- # header name and value based on the config.
13
- def make_header(config = nil, user_agent = nil)
14
- return if config == OPT_OUT
15
- [HEADER_NAME, config || DEFAULT_VALUE]
16
- end
8
+ # Public: generate an x-download-options header.
9
+ #
10
+ # Returns a default header if no configuration is provided, or a
11
+ # header name and value based on the config.
12
+ def self.make_header(config = nil, user_agent = nil)
13
+ return if config == OPT_OUT
14
+ [HEADER_NAME, config || DEFAULT_VALUE]
15
+ end
17
16
 
18
- def validate_config!(config)
19
- return if config.nil? || config == OPT_OUT
20
- raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String)
21
- unless config.casecmp(DEFAULT_VALUE) == 0
22
- raise XDOConfigError.new("Value can only be nil or 'noopen'")
23
- end
17
+ def self.validate_config!(config)
18
+ return if config.nil? || config == OPT_OUT
19
+ raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String)
20
+ unless config.casecmp(DEFAULT_VALUE) == 0
21
+ raise XDOConfigError.new("Value can only be nil or 'noopen'")
24
22
  end
25
23
  end
26
24
  end