secure_headers 6.0.0 → 6.7.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 (38) hide show
  1. checksums.yaml +5 -5
  2. data/.github/dependabot.yml +6 -0
  3. data/.github/workflows/build.yml +24 -0
  4. data/.github/workflows/github-release.yml +28 -0
  5. data/.rubocop.yml +1 -0
  6. data/.ruby-version +1 -1
  7. data/CHANGELOG.md +47 -1
  8. data/Gemfile +4 -1
  9. data/LICENSE +1 -1
  10. data/README.md +70 -30
  11. data/docs/cookies.md +5 -4
  12. data/docs/named_overrides_and_appends.md +3 -6
  13. data/docs/per_action_configuration.md +2 -4
  14. data/docs/upgrading-to-6-0.md +4 -4
  15. data/lib/secure_headers/configuration.rb +24 -16
  16. data/lib/secure_headers/headers/content_security_policy.rb +34 -41
  17. data/lib/secure_headers/headers/content_security_policy_config.rb +15 -48
  18. data/lib/secure_headers/headers/cookie.rb +9 -3
  19. data/lib/secure_headers/headers/policy_management.rb +92 -17
  20. data/lib/secure_headers/middleware.rb +0 -6
  21. data/lib/secure_headers/utils/cookies_config.rb +7 -5
  22. data/lib/secure_headers/version.rb +5 -0
  23. data/lib/secure_headers/view_helper.rb +11 -10
  24. data/lib/secure_headers.rb +3 -11
  25. data/lib/tasks/tasks.rake +6 -7
  26. data/secure_headers.gemspec +9 -5
  27. data/spec/lib/secure_headers/configuration_spec.rb +54 -0
  28. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +98 -8
  29. data/spec/lib/secure_headers/headers/cookie_spec.rb +22 -25
  30. data/spec/lib/secure_headers/headers/policy_management_spec.rb +29 -19
  31. data/spec/lib/secure_headers/middleware_spec.rb +0 -19
  32. data/spec/lib/secure_headers/view_helpers_spec.rb +5 -4
  33. data/spec/lib/secure_headers_spec.rb +0 -35
  34. data/spec/spec_helper.rb +10 -1
  35. metadata +13 -12
  36. data/.travis.yml +0 -29
  37. data/lib/secure_headers/headers/public_key_pins.rb +0 -81
  38. data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +0 -38
@@ -7,21 +7,22 @@ module SecureHeaders
7
7
  include PolicyManagement
8
8
 
9
9
  def initialize(config = nil)
10
- @config = if config.is_a?(Hash)
11
- if config[:report_only]
12
- ContentSecurityPolicyReportOnlyConfig.new(config || DEFAULT_CONFIG)
10
+ @config =
11
+ if config.is_a?(Hash)
12
+ if config[:report_only]
13
+ ContentSecurityPolicyReportOnlyConfig.new(config || DEFAULT_CONFIG)
14
+ else
15
+ ContentSecurityPolicyConfig.new(config || DEFAULT_CONFIG)
16
+ end
17
+ elsif config.nil?
18
+ ContentSecurityPolicyConfig.new(DEFAULT_CONFIG)
13
19
  else
14
- ContentSecurityPolicyConfig.new(config || DEFAULT_CONFIG)
20
+ config
15
21
  end
16
- elsif config.nil?
17
- ContentSecurityPolicyConfig.new(DEFAULT_CONFIG)
18
- else
19
- config
20
- end
21
22
 
22
- @preserve_schemes = @config.preserve_schemes
23
- @script_nonce = @config.script_nonce
24
- @style_nonce = @config.style_nonce
23
+ @preserve_schemes = @config[:preserve_schemes]
24
+ @script_nonce = @config[:script_nonce]
25
+ @style_nonce = @config[:style_nonce]
25
26
  end
26
27
 
27
28
  ##
@@ -34,11 +35,12 @@ module SecureHeaders
34
35
  ##
35
36
  # Return the value of the CSP header
36
37
  def value
37
- @value ||= if @config
38
- build_value
39
- else
40
- DEFAULT_VALUE
41
- end
38
+ @value ||=
39
+ if @config
40
+ build_value
41
+ else
42
+ DEFAULT_VALUE
43
+ end
42
44
  end
43
45
 
44
46
  private
@@ -51,14 +53,16 @@ module SecureHeaders
51
53
  def build_value
52
54
  directives.map do |directive_name|
53
55
  case DIRECTIVE_VALUE_TYPES[directive_name]
56
+ when :source_list,
57
+ :require_sri_for_list, # require_sri is a simple set of strings that don't need to deal with symbol casing
58
+ :require_trusted_types_for_list
59
+ build_source_list_directive(directive_name)
54
60
  when :boolean
55
61
  symbol_to_hyphen_case(directive_name) if @config.directive_value(directive_name)
56
62
  when :sandbox_list
57
63
  build_sandbox_list_directive(directive_name)
58
64
  when :media_type_list
59
65
  build_media_type_list_directive(directive_name)
60
- when :source_list
61
- build_source_list_directive(directive_name)
62
66
  end
63
67
  end.compact.join("; ")
64
68
  end
@@ -103,10 +107,15 @@ module SecureHeaders
103
107
  # Returns a string representing a directive.
104
108
  def build_source_list_directive(directive)
105
109
  source_list = @config.directive_value(directive)
106
-
107
110
  if source_list != OPT_OUT && source_list && source_list.any?
108
- normalized_source_list = minify_source_list(directive, source_list)
109
- [symbol_to_hyphen_case(directive), normalized_source_list].join(" ")
111
+ minified_source_list = minify_source_list(directive, source_list).join(" ")
112
+
113
+ if minified_source_list =~ /(\n|;)/
114
+ Kernel.warn("#{directive} contains a #{$1} in #{minified_source_list.inspect} which will raise an error in future versions. It has been replaced with a blank space.")
115
+ end
116
+
117
+ escaped_source_list = minified_source_list.gsub(/[\n;]/, " ")
118
+ [symbol_to_hyphen_case(directive), escaped_source_list].join(" ").strip
110
119
  end
111
120
  end
112
121
 
@@ -124,7 +133,7 @@ module SecureHeaders
124
133
  unless directive == REPORT_URI || @preserve_schemes
125
134
  source_list = strip_source_schemes(source_list)
126
135
  end
127
- dedup_source_list(source_list)
136
+ source_list.uniq
128
137
  end
129
138
  end
130
139
 
@@ -142,23 +151,6 @@ module SecureHeaders
142
151
  end
143
152
  end
144
153
 
145
- # Removes duplicates and sources that already match an existing wild card.
146
- #
147
- # e.g. *.github.com asdf.github.com becomes *.github.com
148
- def dedup_source_list(sources)
149
- sources = sources.uniq
150
- wild_sources = sources.select { |source| source =~ STAR_REGEXP }
151
-
152
- if wild_sources.any?
153
- sources.reject do |source|
154
- !wild_sources.include?(source) &&
155
- wild_sources.any? { |pattern| File.fnmatch(pattern, source) }
156
- end
157
- else
158
- sources
159
- end
160
- end
161
-
162
154
  # Private: append a nonce to the script/style directories if script_nonce
163
155
  # or style_nonce are provided.
164
156
  def populate_nonces(directive, source_list)
@@ -179,7 +171,8 @@ module SecureHeaders
179
171
  # unsafe-inline, this is more concise.
180
172
  def append_nonce(source_list, nonce)
181
173
  if nonce
182
- source_list.push("'nonce-#{nonce}'", UNSAFE_INLINE)
174
+ source_list.push("'nonce-#{nonce}'")
175
+ source_list.push(UNSAFE_INLINE) unless @config[:disable_nonce_backwards_compatibility]
183
176
  end
184
177
 
185
178
  source_list
@@ -1,56 +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
- @object_src = nil
31
- @plugin_types = nil
32
- @preserve_schemes = nil
33
- @report_only = nil
34
- @report_uri = nil
35
- @sandbox = nil
36
- @script_nonce = nil
37
- @script_src = nil
38
- @style_nonce = nil
39
- @style_src = nil
40
- @worker_src = nil
41
- @upgrade_insecure_requests = nil
5
+ @config = {}
42
6
 
43
7
  from_hash(hash)
44
8
  end
45
9
 
10
+ def initialize_copy(hash)
11
+ @config = hash.to_h
12
+ end
13
+
46
14
  def update_directive(directive, value)
47
- self.send("#{directive}=", value)
15
+ @config[directive] = value
48
16
  end
49
17
 
50
18
  def directive_value(directive)
51
- if self.class.attrs.include?(directive)
52
- self.send(directive)
53
- end
19
+ # No need to check attrs, as we only assign valid keys
20
+ @config[directive]
54
21
  end
55
22
 
56
23
  def merge(new_hash)
@@ -68,10 +35,7 @@ module SecureHeaders
68
35
  end
69
36
 
70
37
  def to_h
71
- self.class.attrs.each_with_object({}) do |key, hash|
72
- value = self.send(key)
73
- hash[key] = value unless value.nil?
74
- end
38
+ @config.dup
75
39
  end
76
40
 
77
41
  def dup
@@ -104,8 +68,11 @@ module SecureHeaders
104
68
 
105
69
  def write_attribute(attr, value)
106
70
  value = value.dup if PolicyManagement::DIRECTIVE_VALUE_TYPES[attr] == :source_list
107
- attr_variable = "@#{attr}"
108
- self.instance_variable_set(attr_variable, value)
71
+ if value.nil?
72
+ @config.delete(attr)
73
+ else
74
+ @config[attr] = value
75
+ end
109
76
  end
110
77
  end
111
78
 
@@ -113,7 +80,7 @@ module SecureHeaders
113
80
  class ContentSecurityPolicyConfig
114
81
  HEADER_NAME = "Content-Security-Policy".freeze
115
82
 
116
- ATTRS = PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES
83
+ ATTRS = Set.new(PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES)
117
84
  def self.attrs
118
85
  ATTRS
119
86
  end
@@ -80,9 +80,9 @@ module SecureHeaders
80
80
  end
81
81
 
82
82
  def conditionally_flag?(configuration)
83
- if(Array(configuration[:only]).any? && (Array(configuration[:only]) & parsed_cookie.keys).any?)
83
+ if (Array(configuration[:only]).any? && (Array(configuration[:only]) & parsed_cookie.keys).any?)
84
84
  true
85
- elsif(Array(configuration[:except]).any? && (Array(configuration[:except]) & parsed_cookie.keys).none?)
85
+ elsif (Array(configuration[:except]).any? && (Array(configuration[:except]) & parsed_cookie.keys).none?)
86
86
  true
87
87
  else
88
88
  false
@@ -94,12 +94,14 @@ module SecureHeaders
94
94
  "SameSite=Lax"
95
95
  elsif flag_samesite_strict?
96
96
  "SameSite=Strict"
97
+ elsif flag_samesite_none?
98
+ "SameSite=None"
97
99
  end
98
100
  end
99
101
 
100
102
  def flag_samesite?
101
103
  return false if config == OPT_OUT || config[:samesite] == OPT_OUT
102
- flag_samesite_lax? || flag_samesite_strict?
104
+ flag_samesite_lax? || flag_samesite_strict? || flag_samesite_none?
103
105
  end
104
106
 
105
107
  def flag_samesite_lax?
@@ -110,6 +112,10 @@ module SecureHeaders
110
112
  flag_samesite_enforcement?(:strict)
111
113
  end
112
114
 
115
+ def flag_samesite_none?
116
+ flag_samesite_enforcement?(:none)
117
+ end
118
+
113
119
  def flag_samesite_enforcement?(mode)
114
120
  return unless config[:samesite]
115
121
 
@@ -1,4 +1,7 @@
1
1
  # frozen_string_literal: true
2
+
3
+ require "set"
4
+
2
5
  module SecureHeaders
3
6
  module PolicyManagement
4
7
  def self.included(base)
@@ -68,20 +71,44 @@ module SecureHeaders
68
71
 
69
72
  # All the directives currently under consideration for CSP level 3.
70
73
  # https://w3c.github.io/webappsec/specs/CSP2/
71
- BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content
72
74
  MANIFEST_SRC = :manifest_src
75
+ NAVIGATE_TO = :navigate_to
76
+ PREFETCH_SRC = :prefetch_src
77
+ REQUIRE_SRI_FOR = :require_sri_for
73
78
  UPGRADE_INSECURE_REQUESTS = :upgrade_insecure_requests
74
79
  WORKER_SRC = :worker_src
80
+ SCRIPT_SRC_ELEM = :script_src_elem
81
+ SCRIPT_SRC_ATTR = :script_src_attr
82
+ STYLE_SRC_ELEM = :style_src_elem
83
+ STYLE_SRC_ATTR = :style_src_attr
75
84
 
76
85
  DIRECTIVES_3_0 = [
77
86
  DIRECTIVES_2_0,
78
- BLOCK_ALL_MIXED_CONTENT,
79
87
  MANIFEST_SRC,
88
+ NAVIGATE_TO,
89
+ PREFETCH_SRC,
90
+ REQUIRE_SRI_FOR,
80
91
  WORKER_SRC,
81
- UPGRADE_INSECURE_REQUESTS
92
+ UPGRADE_INSECURE_REQUESTS,
93
+ SCRIPT_SRC_ELEM,
94
+ SCRIPT_SRC_ATTR,
95
+ STYLE_SRC_ELEM,
96
+ STYLE_SRC_ATTR
82
97
  ].flatten.freeze
83
98
 
84
- ALL_DIRECTIVES = (DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0).uniq.sort
99
+ # Experimental directives - these vary greatly in support
100
+ # See MDN for details.
101
+ # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/trusted-types
102
+ TRUSTED_TYPES = :trusted_types
103
+ # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/require-trusted-types-for
104
+ REQUIRE_TRUSTED_TYPES_FOR = :require_trusted_types_for
105
+
106
+ DIRECTIVES_EXPERIMENTAL = [
107
+ TRUSTED_TYPES,
108
+ REQUIRE_TRUSTED_TYPES_FOR,
109
+ ].flatten.freeze
110
+
111
+ ALL_DIRECTIVES = (DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_EXPERIMENTAL).uniq.sort
85
112
 
86
113
  # Think of default-src and report-uri as the beginning and end respectively,
87
114
  # everything else is in between.
@@ -89,7 +116,6 @@ module SecureHeaders
89
116
 
90
117
  DIRECTIVE_VALUE_TYPES = {
91
118
  BASE_URI => :source_list,
92
- BLOCK_ALL_MIXED_CONTENT => :boolean,
93
119
  CHILD_SRC => :source_list,
94
120
  CONNECT_SRC => :source_list,
95
121
  DEFAULT_SRC => :source_list,
@@ -100,14 +126,23 @@ module SecureHeaders
100
126
  IMG_SRC => :source_list,
101
127
  MANIFEST_SRC => :source_list,
102
128
  MEDIA_SRC => :source_list,
129
+ NAVIGATE_TO => :source_list,
103
130
  OBJECT_SRC => :source_list,
104
131
  PLUGIN_TYPES => :media_type_list,
132
+ REQUIRE_SRI_FOR => :require_sri_for_list,
133
+ REQUIRE_TRUSTED_TYPES_FOR => :require_trusted_types_for_list,
105
134
  REPORT_URI => :source_list,
135
+ PREFETCH_SRC => :source_list,
106
136
  SANDBOX => :sandbox_list,
107
137
  SCRIPT_SRC => :source_list,
138
+ SCRIPT_SRC_ELEM => :source_list,
139
+ SCRIPT_SRC_ATTR => :source_list,
108
140
  STYLE_SRC => :source_list,
141
+ STYLE_SRC_ELEM => :source_list,
142
+ STYLE_SRC_ATTR => :source_list,
143
+ TRUSTED_TYPES => :source_list,
109
144
  WORKER_SRC => :source_list,
110
- UPGRADE_INSECURE_REQUESTS => :boolean
145
+ UPGRADE_INSECURE_REQUESTS => :boolean,
111
146
  }.freeze
112
147
 
113
148
  # These are directives that don't have use a source list, and hence do not
@@ -122,7 +157,8 @@ module SecureHeaders
122
157
  BASE_URI,
123
158
  FORM_ACTION,
124
159
  FRAME_ANCESTORS,
125
- REPORT_URI
160
+ NAVIGATE_TO,
161
+ REPORT_URI,
126
162
  ]
127
163
 
128
164
  FETCH_SOURCES = ALL_DIRECTIVES - NON_FETCH_SOURCES - NON_SOURCE_LIST_SOURCES
@@ -140,7 +176,8 @@ module SecureHeaders
140
176
 
141
177
  META_CONFIGS = [
142
178
  :report_only,
143
- :preserve_schemes
179
+ :preserve_schemes,
180
+ :disable_nonce_backwards_compatibility
144
181
  ].freeze
145
182
 
146
183
  NONCES = [
@@ -148,6 +185,9 @@ module SecureHeaders
148
185
  :style_nonce
149
186
  ].freeze
150
187
 
188
+ REQUIRE_SRI_FOR_VALUES = Set.new(%w(script style))
189
+ REQUIRE_TRUSTED_TYPES_FOR_VALUES = Set.new(%w('script'))
190
+
151
191
  module ClassMethods
152
192
  # Public: generate a header name, value array that is user-agent-aware.
153
193
  #
@@ -198,7 +238,7 @@ module SecureHeaders
198
238
  #
199
239
  # raises an error if the original config is OPT_OUT
200
240
  #
201
- # 1. for non-source-list values (report_only, block_all_mixed_content, upgrade_insecure_requests),
241
+ # 1. for non-source-list values (report_only, upgrade_insecure_requests),
202
242
  # additions will overwrite the original value.
203
243
  # 2. if a value in additions does not exist in the original config, the
204
244
  # default-src value is included to match original behavior.
@@ -241,7 +281,9 @@ module SecureHeaders
241
281
  def list_directive?(directive)
242
282
  source_list?(directive) ||
243
283
  sandbox_list?(directive) ||
244
- media_type_list?(directive)
284
+ media_type_list?(directive) ||
285
+ require_sri_for_list?(directive) ||
286
+ require_trusted_types_for_list?(directive)
245
287
  end
246
288
 
247
289
  # For each directive in additions that does not exist in the original config,
@@ -249,11 +291,12 @@ module SecureHeaders
249
291
  def populate_fetch_source_with_default!(original, additions)
250
292
  # in case we would be appending to an empty directive, fill it with the default-src value
251
293
  additions.each_key do |directive|
252
- directive = if directive.to_s.end_with?("_nonce")
253
- directive.to_s.gsub(/_nonce/, "_src").to_sym
254
- else
255
- directive
256
- end
294
+ directive =
295
+ if directive.to_s.end_with?("_nonce")
296
+ directive.to_s.gsub(/_nonce/, "_src").to_sym
297
+ else
298
+ directive
299
+ end
257
300
  # Don't set a default if directive has an existing value
258
301
  next if original[directive]
259
302
  if FETCH_SOURCES.include?(directive)
@@ -274,11 +317,21 @@ module SecureHeaders
274
317
  DIRECTIVE_VALUE_TYPES[directive] == :media_type_list
275
318
  end
276
319
 
320
+ def require_sri_for_list?(directive)
321
+ DIRECTIVE_VALUE_TYPES[directive] == :require_sri_for_list
322
+ end
323
+
324
+ def require_trusted_types_for_list?(directive)
325
+ DIRECTIVE_VALUE_TYPES[directive] == :require_trusted_types_for_list
326
+ end
327
+
277
328
  # Private: Validates that the configuration has a valid type, or that it is a valid
278
329
  # source expression.
279
330
  def validate_directive!(directive, value)
280
331
  ensure_valid_directive!(directive)
281
332
  case ContentSecurityPolicy::DIRECTIVE_VALUE_TYPES[directive]
333
+ when :source_list
334
+ validate_source_expression!(directive, value)
282
335
  when :boolean
283
336
  unless boolean?(value)
284
337
  raise ContentSecurityPolicyConfigError.new("#{directive} must be a boolean. Found #{value.class} value")
@@ -287,8 +340,10 @@ module SecureHeaders
287
340
  validate_sandbox_expression!(directive, value)
288
341
  when :media_type_list
289
342
  validate_media_type_expression!(directive, value)
290
- when :source_list
291
- validate_source_expression!(directive, value)
343
+ when :require_sri_for_list
344
+ validate_require_sri_source_expression!(directive, value)
345
+ when :require_trusted_types_for_list
346
+ validate_require_trusted_types_for_source_expression!(directive, value)
292
347
  else
293
348
  raise ContentSecurityPolicyConfigError.new("Unknown directive #{directive}")
294
349
  end
@@ -323,6 +378,26 @@ module SecureHeaders
323
378
  end
324
379
  end
325
380
 
381
+ # Private: validates that a require sri for expression:
382
+ # 1. is an array of strings
383
+ # 2. is a subset of ["string", "style"]
384
+ def validate_require_sri_source_expression!(directive, require_sri_for_expression)
385
+ ensure_array_of_strings!(directive, require_sri_for_expression)
386
+ unless require_sri_for_expression.to_set.subset?(REQUIRE_SRI_FOR_VALUES)
387
+ raise ContentSecurityPolicyConfigError.new(%(require-sri for must be a subset of #{REQUIRE_SRI_FOR_VALUES.to_a} but was #{require_sri_for_expression}))
388
+ end
389
+ end
390
+
391
+ # Private: validates that a require trusted types for expression:
392
+ # 1. is an array of strings
393
+ # 2. is a subset of ["'script'"]
394
+ def validate_require_trusted_types_for_source_expression!(directive, require_trusted_types_for_expression)
395
+ ensure_array_of_strings!(directive, require_trusted_types_for_expression)
396
+ unless require_trusted_types_for_expression.to_set.subset?(REQUIRE_TRUSTED_TYPES_FOR_VALUES)
397
+ 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}))
398
+ end
399
+ end
400
+
326
401
  # Private: validates that a source expression:
327
402
  # 1. is an array of strings
328
403
  # 2. does not contain any deprecated, now invalid values (inline, eval, self, none)
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  module SecureHeaders
3
3
  class Middleware
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"
5
-
6
4
  def initialize(app)
7
5
  @app = app
8
6
  end
@@ -13,10 +11,6 @@ module SecureHeaders
13
11
  status, headers, response = @app.call(env)
14
12
 
15
13
  config = SecureHeaders.config_for(req)
16
- if config.hpkp_report_host == req.host
17
- Kernel.warn(HPKP_SAME_HOST_WARNING)
18
- end
19
-
20
14
  flag_cookies!(headers, override_secure(env, config.cookies)) unless config.cookies == OPT_OUT
21
15
  headers.merge!(SecureHeaders.header_hash_for(req))
22
16
  [status, headers, response]
@@ -43,10 +43,12 @@ module SecureHeaders
43
43
 
44
44
  # when configuring with booleans, only one enforcement is permitted
45
45
  def validate_samesite_boolean_config!
46
- if config[:samesite].key?(:lax) && config[:samesite][:lax].is_a?(TrueClass) && config[:samesite].key?(:strict)
47
- raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax and strict enforcement is not permitted.")
48
- elsif config[:samesite].key?(:strict) && config[:samesite][:strict].is_a?(TrueClass) && config[:samesite].key?(:lax)
49
- raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax and strict enforcement is not permitted.")
46
+ if config[:samesite].key?(:lax) && config[:samesite][:lax].is_a?(TrueClass) && (config[:samesite].key?(:strict) || config[:samesite].key?(:none))
47
+ 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.")
48
+ elsif config[:samesite].key?(:strict) && config[:samesite][:strict].is_a?(TrueClass) && (config[:samesite].key?(:lax) || config[:samesite].key?(:none))
49
+ 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.")
50
+ elsif config[:samesite].key?(:none) && config[:samesite][:none].is_a?(TrueClass) && (config[:samesite].key?(:lax) || config[:samesite].key?(:strict))
51
+ 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.")
50
52
  end
51
53
  end
52
54
 
@@ -65,7 +67,7 @@ module SecureHeaders
65
67
 
66
68
  def validate_hash_or_true_or_opt_out!(attribute)
67
69
  if !(is_hash?(config[attribute]) || is_true_or_opt_out?(config[attribute]))
68
- raise CookiesConfigError.new("#{attribute} cookie config must be a hash or boolean")
70
+ raise CookiesConfigError.new("#{attribute} cookie config must be a hash, true, or SecureHeaders::OPT_OUT")
69
71
  end
70
72
  end
71
73
 
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SecureHeaders
4
+ VERSION = "6.7.0"
5
+ end
@@ -21,7 +21,7 @@ module SecureHeaders
21
21
  def nonced_stylesheet_link_tag(*args, &block)
22
22
  opts = extract_options(args).merge(nonce: _content_security_policy_nonce(:style))
23
23
 
24
- stylesheet_link_tag(*args, opts, &block)
24
+ stylesheet_link_tag(*args, **opts, &block)
25
25
  end
26
26
 
27
27
  # Public: create a script tag using the content security policy nonce.
@@ -39,7 +39,7 @@ module SecureHeaders
39
39
  def nonced_javascript_include_tag(*args, &block)
40
40
  opts = extract_options(args).merge(nonce: _content_security_policy_nonce(:script))
41
41
 
42
- javascript_include_tag(*args, opts, &block)
42
+ javascript_include_tag(*args, **opts, &block)
43
43
  end
44
44
 
45
45
  # Public: create a script Webpacker pack tag using the content security policy nonce.
@@ -49,7 +49,7 @@ module SecureHeaders
49
49
  def nonced_javascript_pack_tag(*args, &block)
50
50
  opts = extract_options(args).merge(nonce: _content_security_policy_nonce(:script))
51
51
 
52
- javascript_pack_tag(*args, opts, &block)
52
+ javascript_pack_tag(*args, **opts, &block)
53
53
  end
54
54
 
55
55
  # Public: create a stylesheet Webpacker link tag using the content security policy nonce.
@@ -59,7 +59,7 @@ module SecureHeaders
59
59
  def nonced_stylesheet_pack_tag(*args, &block)
60
60
  opts = extract_options(args).merge(nonce: _content_security_policy_nonce(:style))
61
61
 
62
- stylesheet_pack_tag(*args, opts, &block)
62
+ stylesheet_pack_tag(*args, **opts, &block)
63
63
  end
64
64
 
65
65
  # Public: use the content security policy nonce for this request directly.
@@ -147,12 +147,13 @@ module SecureHeaders
147
147
 
148
148
  def nonced_tag(type, content_or_options, block)
149
149
  options = {}
150
- content = if block
151
- options = content_or_options
152
- capture(&block)
153
- else
154
- content_or_options.html_safe # :'(
155
- end
150
+ content =
151
+ if block
152
+ options = content_or_options
153
+ capture(&block)
154
+ else
155
+ content_or_options.html_safe # :'(
156
+ end
156
157
  content_tag type, content, options.merge(nonce: _content_security_policy_nonce(type))
157
158
  end
158
159
 
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  require "secure_headers/hash_helper"
3
3
  require "secure_headers/headers/cookie"
4
- require "secure_headers/headers/public_key_pins"
5
4
  require "secure_headers/headers/content_security_policy"
6
5
  require "secure_headers/headers/x_frame_options"
7
6
  require "secure_headers/headers/strict_transport_security"
@@ -18,8 +17,7 @@ require "secure_headers/view_helper"
18
17
  require "singleton"
19
18
  require "secure_headers/configuration"
20
19
 
21
- # All headers (except for hpkp) have a default value. Provide SecureHeaders::OPT_OUT
22
- # or ":optout_of_protection" as a config value to disable a given header
20
+ # Provide SecureHeaders::OPT_OUT as a config value to disable a given header
23
21
  module SecureHeaders
24
22
  class NoOpHeaderConfig
25
23
  include Singleton
@@ -51,10 +49,6 @@ module SecureHeaders
51
49
  HTTPS = "https".freeze
52
50
  CSP = ContentSecurityPolicy
53
51
 
54
- # Headers set on http requests (excludes STS and HPKP)
55
- HTTPS_HEADER_CLASSES =
56
- [StrictTransportSecurity, PublicKeyPins].freeze
57
-
58
52
  class << self
59
53
  # Public: override a given set of directives for the current request. If a
60
54
  # value already exists for a given directive, it will be overridden.
@@ -138,7 +132,7 @@ module SecureHeaders
138
132
  # Public: Builds the hash of headers that should be applied base on the
139
133
  # request.
140
134
  #
141
- # StrictTransportSecurity and PublicKeyPins are not applied to http requests.
135
+ # StrictTransportSecurity is not applied to http requests.
142
136
  # See #config_for to determine which config is used for a given request.
143
137
  #
144
138
  # Returns a hash of header names => header values. The value
@@ -151,9 +145,7 @@ module SecureHeaders
151
145
  headers = config.generate_headers
152
146
 
153
147
  if request.scheme != HTTPS
154
- HTTPS_HEADER_CLASSES.each do |klass|
155
- headers.delete(klass::HEADER_NAME)
156
- end
148
+ headers.delete(StrictTransportSecurity::HEADER_NAME)
157
149
  end
158
150
  headers
159
151
  end
data/lib/tasks/tasks.rake CHANGED
@@ -20,10 +20,11 @@ namespace :secure_headers do
20
20
  (is_erb?(filename) && inline_script =~ /<%.*%>/)
21
21
  end
22
22
 
23
- def find_inline_content(filename, regex, hashes)
23
+ def find_inline_content(filename, regex, hashes, strip_trailing_whitespace)
24
24
  file = File.read(filename)
25
25
  file.scan(regex) do # TODO don't use gsub
26
26
  inline_script = Regexp.last_match.captures.last
27
+ inline_script.gsub!(/(\r?\n)[\t ]+\z/, '\1') if strip_trailing_whitespace
27
28
  if dynamic_content?(filename, inline_script)
28
29
  puts "Looks like there's some dynamic content inside of a tag :-/"
29
30
  puts "That pretty much means the hash value will never match."
@@ -38,9 +39,8 @@ namespace :secure_headers do
38
39
  def generate_inline_script_hashes(filename)
39
40
  hashes = []
40
41
 
41
- [INLINE_SCRIPT_REGEX, INLINE_HASH_SCRIPT_HELPER_REGEX].each do |regex|
42
- find_inline_content(filename, regex, hashes)
43
- end
42
+ find_inline_content(filename, INLINE_SCRIPT_REGEX, hashes, false)
43
+ find_inline_content(filename, INLINE_HASH_SCRIPT_HELPER_REGEX, hashes, true)
44
44
 
45
45
  hashes
46
46
  end
@@ -48,9 +48,8 @@ namespace :secure_headers do
48
48
  def generate_inline_style_hashes(filename)
49
49
  hashes = []
50
50
 
51
- [INLINE_STYLE_REGEX, INLINE_HASH_STYLE_HELPER_REGEX].each do |regex|
52
- find_inline_content(filename, regex, hashes)
53
- end
51
+ find_inline_content(filename, INLINE_STYLE_REGEX, hashes, false)
52
+ find_inline_content(filename, INLINE_HASH_STYLE_HELPER_REGEX, hashes, true)
54
53
 
55
54
  hashes
56
55
  end