secure_headers 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7353ab86b76cba9baabc265f6321990163927fa0
4
- data.tar.gz: 8081e9d2a284a26191899317d1947cde24d80d9f
3
+ metadata.gz: a02f16867ec55f8c168ace664cc63d760785256f
4
+ data.tar.gz: db54c113af8919984c8f382b957834f38210cf3e
5
5
  SHA512:
6
- metadata.gz: 37fd3a8aca07d1ce46bd7cdaf89ddb057d41b94f2f41625287d9a3118d19300708fd33f9d5f029c341a9c80295848101f01558d3dfd370a7bba34c8862e710d6
7
- data.tar.gz: 0d810d24a48f53a45bec5621291475059e521d0d4f640bf1430ad6c86ecf020fea8dd8eb220e600b71c32539d7cfbc1f87567066477deebdf660d8a0161e2f8a
6
+ metadata.gz: 6d9ddcb98a8c646d4e7a8cdb79bf3d8638cae4cb230ff268660e8eab7000fba99bb65e126ce55b3ff5f2a0346013fcf8b769dab4db697c82ab4461d444d0e2bd
7
+ data.tar.gz: b63c79ac3e9b37a4cff698f979f1ca6fd7e92567e35b1f9026be2e68a205e5badff302d512938a4d36517e8ec5b752d4833082e1b733ff73916fbf0b326641aa
data/CHANGELOG.md CHANGED
@@ -1,3 +1,136 @@
1
+ ## 3.2.0 Cookie settings and CSP hash sources
2
+
3
+ ### Cookies
4
+
5
+ SecureHeaders supports `Secure`, `HttpOnly` and [`SameSite`](https://tools.ietf.org/html/draft-west-first-party-cookies-07) cookies. These can be defined in the form of a boolean, or as a Hash for more refined configuration.
6
+
7
+ __Note__: Regardless of the configuration specified, Secure cookies are only enabled for HTTPS requests.
8
+
9
+ #### Boolean-based configuration
10
+
11
+ Boolean-based configuration is intended to globally enable or disable a specific cookie attribute.
12
+
13
+ ```ruby
14
+ config.cookies = {
15
+ secure: true, # mark all cookies as Secure
16
+ httponly: false, # do not mark any cookies as HttpOnly
17
+ }
18
+ ```
19
+
20
+ #### Hash-based configuration
21
+
22
+ Hash-based configuration allows for fine-grained control.
23
+
24
+ ```ruby
25
+ config.cookies = {
26
+ secure: { except: ['_guest'] }, # mark all but the `_guest` cookie as Secure
27
+ httponly: { only: ['_rails_session'] }, # only mark the `_rails_session` cookie as HttpOnly
28
+ }
29
+ ```
30
+
31
+ #### SameSite cookie configuration
32
+
33
+ SameSite cookies permit either `Strict` or `Lax` enforcement mode options.
34
+
35
+ ```ruby
36
+ config.cookies = {
37
+ samesite: {
38
+ strict: true # mark all cookies as SameSite=Strict
39
+ }
40
+ }
41
+ ```
42
+
43
+ `Strict` and `Lax` enforcement modes can also be specified using a Hash.
44
+
45
+ ```ruby
46
+ config.cookies = {
47
+ samesite: {
48
+ strict: { only: ['_rails_session'] },
49
+ lax: { only: ['_guest'] }
50
+ }
51
+ }
52
+ ```
53
+
54
+ #### Hash
55
+
56
+ `script`/`style-src` hashes can be used to whitelist inline content that is static. This has the benefit of allowing inline content without opening up the possibility of dynamic javascript like you would with a `nonce`.
57
+
58
+ You can add hash sources directly to your policy :
59
+
60
+ ```ruby
61
+ ::SecureHeaders::Configuration.default do |config|
62
+ config.csp = {
63
+ default_src: %w('self')
64
+
65
+ # this is a made up value but browsers will show the expected hash in the console.
66
+ script_src: %w(sha256-123456)
67
+ }
68
+ end
69
+ ```
70
+
71
+ You can also use the automated inline script detection/collection/computation of hash source values in your app.
72
+
73
+ ```bash
74
+ rake secure_headers:generate_hashes
75
+ ```
76
+
77
+ This will generate a file (`config/config/secure_headers_generated_hashes.yml` by default, you can override by setting `ENV["secure_headers_generated_hashes_file"]`) containing a mapping of file names with the array of hash values found on that page. When ActionView renders a given file, we check if there are any known hashes for that given file. If so, they are added as values to the header.
78
+
79
+ ```yaml
80
+ ---
81
+ scripts:
82
+ app/views/asdfs/index.html.erb:
83
+ - "'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg='"
84
+ styles:
85
+ app/views/asdfs/index.html.erb:
86
+ - "'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY='"
87
+ - "'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE='"
88
+ ```
89
+
90
+ ##### Helpers
91
+
92
+ **This will not compute dynamic hashes** by design. The output of both helpers will be a plain `script`/`style` tag without modification and the known hashes for a given file will be added to `script-src`/`style-src` when `hashed_javascript_tag` and `hashed_style_tag` are used. You can use `raise_error_on_unrecognized_hash = true` to be extra paranoid that you have precomputed hash values for all of your inline content. By default, this will raise an error in non-production environments.
93
+
94
+ ```erb
95
+ <%= hashed_style_tag do %>
96
+ body {
97
+ background-color: black;
98
+ }
99
+ <% end %>
100
+
101
+ <%= hashed_style_tag do %>
102
+ body {
103
+ font-size: 30px;
104
+ font-color: green;
105
+ }
106
+ <% end %>
107
+
108
+ <%= hashed_javascript_tag do %>
109
+ console.log(1)
110
+ <% end %>
111
+ ```
112
+
113
+ ```
114
+ Content-Security-Policy: ...
115
+ script-src 'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg=' ... ;
116
+ style-src 'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY=' 'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE=' ...;
117
+
118
+ ## 3.1.2 Bug fix for regression
119
+
120
+ See https://github.com/twitter/secureheaders/pull/239
121
+
122
+ This meant that when header caches were regenerated upon calling `SecureHeaders.override(:name)` and using it with `use_secure_headers_override` would result in default values for anything other than CSP/HPKP.
123
+
124
+ ## 3.1.1 Bug fix for regression
125
+
126
+ See https://github.com/twitter/secureheaders/pull/235
127
+
128
+ `idempotent_additions?` would return false when comparing `OPT_OUT` with `OPT_OUT`, causing `header_hash_for` to return a header cache with `{ nil => nil }` which cause the middleware to blow up when `{ nil => nil }` was merged into the rack header hash.
129
+
130
+ This is a regression in 3.1.0 only.
131
+
132
+ Now it returns true. I've added a test case to ensure that `header_hash_for` will never return such an element.
133
+
1
134
  ## 3.1.0 Adding secure cookie support
2
135
 
3
136
  New feature: marking all cookies as secure. Added by @jmera in https://github.com/twitter/secureheaders/pull/231. In the future, we'll probably add the ability to whitelist individual cookies that should not be marked secure. PRs welcome.
data/README.md CHANGED
@@ -15,6 +15,8 @@ The gem will automatically apply several headers that are related to security.
15
15
  - 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)
16
16
  - 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)
17
17
 
18
+ It can also mark all http cookies with the Secure, HttpOnly and SameSite attributes (when configured to do so).
19
+
18
20
  `secure_headers` is a library with a global config, per request overrides, and rack middleware that enables you customize your application settings.
19
21
 
20
22
  ## Use
@@ -29,6 +31,13 @@ All `nil` values will fallback to their default values. `SecureHeaders::OPT_OUT`
29
31
 
30
32
  ```ruby
31
33
  SecureHeaders::Configuration.default do |config|
34
+ config.cookies = {
35
+ secure: true, # mark all cookies as "Secure"
36
+ httponly: true, # mark all cookies as "HttpOnly"
37
+ samesite: {
38
+ strict: true # mark all cookies as SameSite=Strict
39
+ }
40
+ }
32
41
  config.hsts = "max-age=#{20.years.to_i}; includeSubdomains; preload"
33
42
  config.x_frame_options = "DENY"
34
43
  config.x_content_type_options = "nosniff"
@@ -192,24 +201,6 @@ Code | Result
192
201
 
193
202
  #### Nonce
194
203
 
195
- script/style-nonce can be used to whitelist inline content. To do this, call the `SecureHeaders.content_security_policy_nonce` then set the nonce attributes on the various tags.
196
-
197
- Setting a nonce will also set 'unsafe-inline' for browsers that don't support nonces for backwards compatibility. 'unsafe-inline' is ignored if a nonce is present in a directive in compliant browsers.
198
-
199
- ```erb
200
- <script nonce="<%= content_security_policy_nonce %>">
201
- console.log("whitelisted, will execute")
202
- </script>
203
-
204
- <script nonce="lol">
205
- console.log("won't execute, not whitelisted")
206
- </script>
207
-
208
- <script>
209
- console.log("won't execute, not whitelisted")
210
- </script>
211
- ```
212
-
213
204
  You can use a view helper to automatically add nonces to script tags:
214
205
 
215
206
  ```erb
@@ -237,9 +228,93 @@ body {
237
228
  </style>
238
229
  ```
239
230
 
231
+ ```
232
+
233
+ Content-Security-Policy: ...
234
+ script-src 'nonce-/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=' ...;
235
+ style-src 'nonce-/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=' ...;
236
+ ```
237
+
238
+ `script`/`style-nonce` can be used to whitelist inline content. To do this, call the `content_security_policy_script_nonce` or `content_security_policy_style_nonce` then set the nonce attributes on the various tags.
239
+
240
+ ```erb
241
+ <script nonce="<%= content_security_policy_script_nonce %>">
242
+ console.log("whitelisted, will execute")
243
+ </script>
244
+
245
+ <script nonce="lol">
246
+ console.log("won't execute, not whitelisted")
247
+ </script>
248
+
249
+ <script>
250
+ console.log("won't execute, not whitelisted")
251
+ </script>
252
+ ```
253
+
240
254
  #### Hash
241
255
 
242
- The hash feature has been removed, for now.
256
+ `script`/`style-src` hashes can be used to whitelist inline content that is static. This has the benefit of allowing inline content without opening up the possibility of dynamic javascript like you would with a `nonce`.
257
+
258
+ You can add hash sources directly to your policy :
259
+
260
+ ```ruby
261
+ ::SecureHeaders::Configuration.default do |config|
262
+ config.csp = {
263
+ default_src: %w('self')
264
+
265
+ # this is a made up value but browsers will show the expected hash in the console.
266
+ script_src: %w(sha256-123456)
267
+ }
268
+ end
269
+ ```
270
+
271
+ You can also use the automated inline script detection/collection/computation of hash source values in your app.
272
+
273
+ ```bash
274
+ rake secure_headers:generate_hashes
275
+ ```
276
+
277
+ This will generate a file (`config/config/secure_headers_generated_hashes.yml` by default, you can override by setting `ENV["secure_headers_generated_hashes_file"]`) containing a mapping of file names with the array of hash values found on that page. When ActionView renders a given file, we check if there are any known hashes for that given file. If so, they are added as values to the header.
278
+
279
+ ```yaml
280
+ ---
281
+ scripts:
282
+ app/views/asdfs/index.html.erb:
283
+ - "'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg='"
284
+ styles:
285
+ app/views/asdfs/index.html.erb:
286
+ - "'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY='"
287
+ - "'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE='"
288
+ ```
289
+
290
+ ##### Helpers
291
+
292
+ **This will not compute dynamic hashes** by design. The output of both helpers will be a plain `script`/`style` tag without modification and the known hashes for a given file will be added to `script-src`/`style-src` when `hashed_javascript_tag` and `hashed_style_tag` are used. You can use `raise_error_on_unrecognized_hash = true` to be extra paranoid that you have precomputed hash values for all of your inline content. By default, this will raise an error in non-production environments.
293
+
294
+ ```erb
295
+ <%= hashed_style_tag do %>
296
+ body {
297
+ background-color: black;
298
+ }
299
+ <% end %>
300
+
301
+ <%= hashed_style_tag do %>
302
+ body {
303
+ font-size: 30px;
304
+ font-color: green;
305
+ }
306
+ <% end %>
307
+
308
+ <%= hashed_javascript_tag do %>
309
+ console.log(1)
310
+ <% end %>
311
+ ```
312
+
313
+ ```
314
+ Content-Security-Policy: ...
315
+ script-src 'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg=' ... ;
316
+ style-src 'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY=' 'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE=' ...;
317
+ ```
243
318
 
244
319
  ### Public Key Pins
245
320
 
@@ -261,6 +336,57 @@ config.hpkp = {
261
336
  }
262
337
  ```
263
338
 
339
+ ### Cookies
340
+
341
+ SecureHeaders supports `Secure`, `HttpOnly` and [`SameSite`](https://tools.ietf.org/html/draft-west-first-party-cookies-07) cookies. These can be defined in the form of a boolean, or as a Hash for more refined configuration.
342
+
343
+ __Note__: Regardless of the configuration specified, Secure cookies are only enabled for HTTPS requests.
344
+
345
+ #### Boolean-based configuration
346
+
347
+ Boolean-based configuration is intended to globally enable or disable a specific cookie attribute.
348
+
349
+ ```ruby
350
+ config.cookies = {
351
+ secure: true, # mark all cookies as Secure
352
+ httponly: false, # do not mark any cookies as HttpOnly
353
+ }
354
+ ```
355
+
356
+ #### Hash-based configuration
357
+
358
+ Hash-based configuration allows for fine-grained control.
359
+
360
+ ```ruby
361
+ config.cookies = {
362
+ secure: { except: ['_guest'] }, # mark all but the `_guest` cookie as Secure
363
+ httponly: { only: ['_rails_session'] }, # only mark the `_rails_session` cookie as HttpOnly
364
+ }
365
+ ```
366
+
367
+ #### SameSite cookie configuration
368
+
369
+ SameSite cookies permit either `Strict` or `Lax` enforcement mode options.
370
+
371
+ ```ruby
372
+ config.cookies = {
373
+ samesite: {
374
+ strict: true # mark all cookies as SameSite=Strict
375
+ }
376
+ }
377
+ ```
378
+
379
+ `Strict` and `Lax` enforcement modes can also be specified using a Hash.
380
+
381
+ ```ruby
382
+ config.cookies = {
383
+ samesite: {
384
+ strict: { only: ['_rails_session'] },
385
+ lax: { only: ['_guest'] }
386
+ }
387
+ }
388
+ ```
389
+
264
390
  ### Using with Sinatra
265
391
 
266
392
  Here's an example using SecureHeaders for Sinatra applications:
@@ -1,3 +1,5 @@
1
+ require 'yaml'
2
+
1
3
  module SecureHeaders
2
4
  class Configuration
3
5
  DEFAULT_CONFIG = :default
@@ -71,6 +73,7 @@ module SecureHeaders
71
73
  ALL_HEADER_CLASSES.each do |klass|
72
74
  config.send("#{klass::CONFIG_KEY}=", OPT_OUT)
73
75
  end
76
+ config.dynamic_csp = OPT_OUT
74
77
  end
75
78
 
76
79
  add_configuration(NOOP_CONFIGURATION, noop_config)
@@ -101,9 +104,16 @@ module SecureHeaders
101
104
 
102
105
  attr_writer :hsts, :x_frame_options, :x_content_type_options,
103
106
  :x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies,
104
- :hpkp, :dynamic_csp, :secure_cookies
107
+ :hpkp, :dynamic_csp, :cookies
108
+
109
+ attr_reader :cached_headers, :csp, :dynamic_csp, :cookies
105
110
 
106
- attr_reader :cached_headers, :csp, :dynamic_csp, :secure_cookies
111
+ HASH_CONFIG_FILE = ENV["secure_headers_generated_hashes_file"] || "config/secure_headers_generated_hashes.yml"
112
+ if File.exists?(HASH_CONFIG_FILE)
113
+ config = YAML.safe_load(File.open(HASH_CONFIG_FILE))
114
+ @script_hashes = config["scripts"]
115
+ @style_hashes = config["styles"]
116
+ end
107
117
 
108
118
  def initialize(&block)
109
119
  self.hpkp = OPT_OUT
@@ -116,10 +126,17 @@ module SecureHeaders
116
126
  # Returns a deep-dup'd copy of this configuration.
117
127
  def dup
118
128
  copy = self.class.new
119
- copy.secure_cookies = @secure_cookies
129
+ copy.cookies = @cookies
120
130
  copy.csp = self.class.send(:deep_copy_if_hash, @csp)
121
131
  copy.dynamic_csp = self.class.send(:deep_copy_if_hash, @dynamic_csp)
122
132
  copy.cached_headers = self.class.send(:deep_copy_if_hash, @cached_headers)
133
+ copy.x_content_type_options = @x_content_type_options
134
+ copy.hsts = @hsts
135
+ copy.x_frame_options = @x_frame_options
136
+ copy.x_xss_protection = @x_xss_protection
137
+ copy.x_download_options = @x_download_options
138
+ copy.x_permitted_cross_domain_policies = @x_permitted_cross_domain_policies
139
+ copy.hpkp = @hpkp
123
140
  copy
124
141
  end
125
142
 
@@ -132,6 +149,7 @@ module SecureHeaders
132
149
  end
133
150
 
134
151
  def update_x_frame_options(value)
152
+ @x_frame_options = value
135
153
  self.cached_headers[XFrameOptions::CONFIG_KEY] = XFrameOptions.make_header(value)
136
154
  end
137
155
 
@@ -163,13 +181,19 @@ module SecureHeaders
163
181
  XDownloadOptions.validate_config!(@x_download_options)
164
182
  XPermittedCrossDomainPolicies.validate_config!(@x_permitted_cross_domain_policies)
165
183
  PublicKeyPins.validate_config!(@hpkp)
184
+ Cookie.validate_config!(@cookies)
185
+ end
186
+
187
+ def secure_cookies=(secure_cookies)
188
+ Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#secure_cookies=` is deprecated. Please use `#cookies=` to configure secure cookies instead."
189
+ @cookies = (@cookies || {}).merge(secure: secure_cookies)
166
190
  end
167
191
 
168
192
  protected
169
193
 
170
194
  def csp=(new_csp)
171
195
  if self.dynamic_csp
172
- raise IllegalPolicyModificationError, "You are attempting to modify CSP settings directly. Use dynamic_csp= isntead."
196
+ raise IllegalPolicyModificationError, "You are attempting to modify CSP settings directly. Use dynamic_csp= instead."
173
197
  end
174
198
 
175
199
  @csp = new_csp
@@ -0,0 +1,10 @@
1
+ require 'base64'
2
+
3
+ module SecureHeaders
4
+ module HashHelper
5
+ def hash_source(inline_script, digest = :SHA256)
6
+ base64_hashed_content = Base64.encode64(Digest.const_get(digest).digest(inline_script)).chomp
7
+ "'#{digest.to_s.downcase}-#{base64_hashed_content}'"
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,126 @@
1
+ require 'cgi'
2
+ require 'secure_headers/utils/cookies_config'
3
+
4
+ module SecureHeaders
5
+ class CookiesConfigError < StandardError; end
6
+ class Cookie
7
+
8
+ class << self
9
+ def validate_config!(config)
10
+ CookiesConfig.new(config).validate!
11
+ end
12
+ end
13
+
14
+ attr_reader :raw_cookie, :config
15
+
16
+ def initialize(cookie, config)
17
+ @raw_cookie = cookie
18
+ @config = config
19
+ @attributes = {
20
+ httponly: nil,
21
+ samesite: nil,
22
+ secure: nil,
23
+ }
24
+
25
+ parse(cookie)
26
+ end
27
+
28
+ def to_s
29
+ @raw_cookie.dup.tap do |c|
30
+ c << "; secure" if secure?
31
+ c << "; HttpOnly" if httponly?
32
+ c << "; #{samesite_cookie}" if samesite?
33
+ end
34
+ end
35
+
36
+ def secure?
37
+ flag_cookie?(:secure) && !already_flagged?(:secure)
38
+ end
39
+
40
+ def httponly?
41
+ flag_cookie?(:httponly) && !already_flagged?(:httponly)
42
+ end
43
+
44
+ def samesite?
45
+ flag_samesite? && !already_flagged?(:samesite)
46
+ end
47
+
48
+ private
49
+
50
+ def parsed_cookie
51
+ @parsed_cookie ||= CGI::Cookie.parse(raw_cookie)
52
+ end
53
+
54
+ def already_flagged?(attribute)
55
+ @attributes[attribute]
56
+ end
57
+
58
+ def flag_cookie?(attribute)
59
+ case config[attribute]
60
+ when TrueClass
61
+ true
62
+ when Hash
63
+ conditionally_flag?(config[attribute])
64
+ else
65
+ false
66
+ end
67
+ end
68
+
69
+ def conditionally_flag?(configuration)
70
+ if(Array(configuration[:only]).any? && (Array(configuration[:only]) & parsed_cookie.keys).any?)
71
+ true
72
+ elsif(Array(configuration[:except]).any? && (Array(configuration[:except]) & parsed_cookie.keys).none?)
73
+ true
74
+ else
75
+ false
76
+ end
77
+ end
78
+
79
+ def samesite_cookie
80
+ if flag_samesite_lax?
81
+ "SameSite=Lax"
82
+ elsif flag_samesite_strict?
83
+ "SameSite=Strict"
84
+ end
85
+ end
86
+
87
+ def flag_samesite?
88
+ flag_samesite_lax? || flag_samesite_strict?
89
+ end
90
+
91
+ def flag_samesite_lax?
92
+ flag_samesite_enforcement?(:lax)
93
+ end
94
+
95
+ def flag_samesite_strict?
96
+ flag_samesite_enforcement?(:strict)
97
+ end
98
+
99
+ def flag_samesite_enforcement?(mode)
100
+ return unless config[:samesite]
101
+
102
+ case config[:samesite][mode]
103
+ when Hash
104
+ conditionally_flag?(config[:samesite][mode])
105
+ when TrueClass
106
+ true
107
+ else
108
+ false
109
+ end
110
+ end
111
+
112
+ def parse(cookie)
113
+ return unless cookie
114
+
115
+ cookie.split(/[;,]\s?/).each do |pairs|
116
+ name, values = pairs.split('=',2)
117
+ name = CGI.unescape(name)
118
+
119
+ attribute = name.downcase.to_sym
120
+ if @attributes.has_key?(attribute)
121
+ @attributes[attribute] = values || true
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -196,6 +196,7 @@ module SecureHeaders
196
196
  # additions = { script_src: %w(google.com)} then idempotent_additions? would return
197
197
  # because google.com is already in the config.
198
198
  def idempotent_additions?(config, additions)
199
+ return true if config == OPT_OUT && additions == OPT_OUT
199
200
  return false if config == OPT_OUT
200
201
  config == combine_policies(config, additions)
201
202
  end
@@ -1,7 +1,5 @@
1
1
  module SecureHeaders
2
2
  class Middleware
3
- SECURE_COOKIE_REGEXP = /;\s*secure\s*(;|$)/i.freeze
4
-
5
3
  def initialize(app)
6
4
  @app = app
7
5
  end
@@ -12,7 +10,7 @@ module SecureHeaders
12
10
  status, headers, response = @app.call(env)
13
11
 
14
12
  config = SecureHeaders.config_for(req)
15
- flag_cookies_as_secure!(headers) if config.secure_cookies
13
+ flag_cookies!(headers, override_secure(env, config.cookies)) if config.cookies
16
14
  headers.merge!(SecureHeaders.header_hash_for(req))
17
15
  [status, headers, response]
18
16
  end
@@ -20,19 +18,35 @@ module SecureHeaders
20
18
  private
21
19
 
22
20
  # inspired by https://github.com/tobmatth/rack-ssl-enforcer/blob/6c014/lib/rack/ssl-enforcer.rb#L183-L194
23
- def flag_cookies_as_secure!(headers)
21
+ def flag_cookies!(headers, config)
24
22
  if cookies = headers['Set-Cookie']
25
23
  # Support Rails 2.3 / Rack 1.1 arrays as headers
26
24
  cookies = cookies.split("\n") unless cookies.is_a?(Array)
27
25
 
28
26
  headers['Set-Cookie'] = cookies.map do |cookie|
29
- if cookie !~ SECURE_COOKIE_REGEXP
30
- "#{cookie}; secure"
31
- else
32
- cookie
33
- end
27
+ SecureHeaders::Cookie.new(cookie, config).to_s
34
28
  end.join("\n")
35
29
  end
36
30
  end
31
+
32
+ # disable Secure cookies for non-https requests
33
+ def override_secure(env, config = {})
34
+ if scheme(env) != 'https'
35
+ config.merge!(secure: false)
36
+ end
37
+
38
+ config
39
+ end
40
+
41
+ # derived from https://github.com/tobmatth/rack-ssl-enforcer/blob/6c014/lib/rack/ssl-enforcer.rb#L119
42
+ def scheme(env)
43
+ if env['HTTPS'] == 'on' || env['HTTP_X_SSL_REQUEST'] == 'on'
44
+ 'https'
45
+ elsif env['HTTP_X_FORWARDED_PROTO']
46
+ env['HTTP_X_FORWARDED_PROTO'].split(',')[0]
47
+ else
48
+ env['rack.url_scheme']
49
+ end
50
+ end
37
51
  end
38
52
  end
@@ -13,6 +13,10 @@ if defined?(Rails::Railtie)
13
13
  Rails.application.config.middleware.insert_before 0, SecureHeaders::Middleware
14
14
  end
15
15
 
16
+ rake_tasks do
17
+ load File.expand_path(File.join('..', '..', 'lib', 'tasks', 'tasks.rake'), File.dirname(__FILE__))
18
+ end
19
+
16
20
  initializer "secure_headers.action_controller" do
17
21
  ActiveSupport.on_load(:action_controller) do
18
22
  include SecureHeaders