secure_headers 3.1.2 → 3.2.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of secure_headers might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +117 -0
- data/README.md +144 -21
- data/lib/secure_headers/configuration.rb +19 -4
- data/lib/secure_headers/hash_helper.rb +10 -0
- data/lib/secure_headers/headers/cookie.rb +126 -0
- data/lib/secure_headers/middleware.rb +23 -9
- data/lib/secure_headers/railtie.rb +4 -0
- data/lib/secure_headers/utils/cookies_config.rb +94 -0
- data/lib/secure_headers/view_helper.rb +64 -0
- data/lib/secure_headers.rb +2 -0
- data/lib/tasks/tasks.rake +81 -0
- data/secure_headers.gemspec +1 -1
- data/spec/lib/secure_headers/configuration_spec.rb +9 -1
- data/spec/lib/secure_headers/headers/cookie_spec.rb +164 -0
- data/spec/lib/secure_headers/middleware_spec.rb +48 -15
- data/spec/lib/secure_headers/view_helpers_spec.rb +125 -0
- data/spec/spec_helper.rb +12 -0
- data/upgrading-to-3-0.md +1 -0
- metadata +10 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a02f16867ec55f8c168ace664cc63d760785256f
|
4
|
+
data.tar.gz: db54c113af8919984c8f382b957834f38210cf3e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6d9ddcb98a8c646d4e7a8cdb79bf3d8638cae4cb230ff268660e8eab7000fba99bb65e126ce55b3ff5f2a0346013fcf8b769dab4db697c82ab4461d444d0e2bd
|
7
|
+
data.tar.gz: b63c79ac3e9b37a4cff698f979f1ca6fd7e92567e35b1f9026be2e68a205e5badff302d512938a4d36517e8ec5b752d4833082e1b733ff73916fbf0b326641aa
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,120 @@
|
|
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
|
+
|
1
118
|
## 3.1.2 Bug fix for regression
|
2
119
|
|
3
120
|
See https://github.com/twitter/secureheaders/pull/239
|
data/README.md
CHANGED
@@ -15,7 +15,7 @@ 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
|
18
|
+
It can also mark all http cookies with the Secure, HttpOnly and SameSite attributes (when configured to do so).
|
19
19
|
|
20
20
|
`secure_headers` is a library with a global config, per request overrides, and rack middleware that enables you customize your application settings.
|
21
21
|
|
@@ -31,7 +31,13 @@ All `nil` values will fallback to their default values. `SecureHeaders::OPT_OUT`
|
|
31
31
|
|
32
32
|
```ruby
|
33
33
|
SecureHeaders::Configuration.default do |config|
|
34
|
-
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
|
+
}
|
35
41
|
config.hsts = "max-age=#{20.years.to_i}; includeSubdomains; preload"
|
36
42
|
config.x_frame_options = "DENY"
|
37
43
|
config.x_content_type_options = "nosniff"
|
@@ -195,24 +201,6 @@ Code | Result
|
|
195
201
|
|
196
202
|
#### Nonce
|
197
203
|
|
198
|
-
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.
|
199
|
-
|
200
|
-
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.
|
201
|
-
|
202
|
-
```erb
|
203
|
-
<script nonce="<%= content_security_policy_nonce %>">
|
204
|
-
console.log("whitelisted, will execute")
|
205
|
-
</script>
|
206
|
-
|
207
|
-
<script nonce="lol">
|
208
|
-
console.log("won't execute, not whitelisted")
|
209
|
-
</script>
|
210
|
-
|
211
|
-
<script>
|
212
|
-
console.log("won't execute, not whitelisted")
|
213
|
-
</script>
|
214
|
-
```
|
215
|
-
|
216
204
|
You can use a view helper to automatically add nonces to script tags:
|
217
205
|
|
218
206
|
```erb
|
@@ -240,9 +228,93 @@ body {
|
|
240
228
|
</style>
|
241
229
|
```
|
242
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
|
+
|
243
254
|
#### Hash
|
244
255
|
|
245
|
-
|
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
|
+
```
|
246
318
|
|
247
319
|
### Public Key Pins
|
248
320
|
|
@@ -264,6 +336,57 @@ config.hpkp = {
|
|
264
336
|
}
|
265
337
|
```
|
266
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
|
+
|
267
390
|
### Using with Sinatra
|
268
391
|
|
269
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
|
@@ -102,9 +104,16 @@ module SecureHeaders
|
|
102
104
|
|
103
105
|
attr_writer :hsts, :x_frame_options, :x_content_type_options,
|
104
106
|
:x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies,
|
105
|
-
:hpkp, :dynamic_csp, :
|
107
|
+
:hpkp, :dynamic_csp, :cookies
|
108
|
+
|
109
|
+
attr_reader :cached_headers, :csp, :dynamic_csp, :cookies
|
106
110
|
|
107
|
-
|
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
|
108
117
|
|
109
118
|
def initialize(&block)
|
110
119
|
self.hpkp = OPT_OUT
|
@@ -117,7 +126,7 @@ module SecureHeaders
|
|
117
126
|
# Returns a deep-dup'd copy of this configuration.
|
118
127
|
def dup
|
119
128
|
copy = self.class.new
|
120
|
-
copy.
|
129
|
+
copy.cookies = @cookies
|
121
130
|
copy.csp = self.class.send(:deep_copy_if_hash, @csp)
|
122
131
|
copy.dynamic_csp = self.class.send(:deep_copy_if_hash, @dynamic_csp)
|
123
132
|
copy.cached_headers = self.class.send(:deep_copy_if_hash, @cached_headers)
|
@@ -172,13 +181,19 @@ module SecureHeaders
|
|
172
181
|
XDownloadOptions.validate_config!(@x_download_options)
|
173
182
|
XPermittedCrossDomainPolicies.validate_config!(@x_permitted_cross_domain_policies)
|
174
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)
|
175
190
|
end
|
176
191
|
|
177
192
|
protected
|
178
193
|
|
179
194
|
def csp=(new_csp)
|
180
195
|
if self.dynamic_csp
|
181
|
-
raise IllegalPolicyModificationError, "You are attempting to modify CSP settings directly. Use dynamic_csp=
|
196
|
+
raise IllegalPolicyModificationError, "You are attempting to modify CSP settings directly. Use dynamic_csp= instead."
|
182
197
|
end
|
183
198
|
|
184
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
|
@@ -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
|
-
|
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
|
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
|
-
|
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
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module SecureHeaders
|
2
|
+
class CookiesConfig
|
3
|
+
|
4
|
+
attr_reader :config
|
5
|
+
|
6
|
+
def initialize(config)
|
7
|
+
@config = config
|
8
|
+
end
|
9
|
+
|
10
|
+
def validate!
|
11
|
+
return if config.nil? || config == SecureHeaders::OPT_OUT
|
12
|
+
|
13
|
+
validate_config!
|
14
|
+
validate_secure_config! if config[:secure]
|
15
|
+
validate_httponly_config! if config[:httponly]
|
16
|
+
validate_samesite_config! if config[:samesite]
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def validate_config!
|
22
|
+
raise CookiesConfigError.new("config must be a hash.") unless is_hash?(config)
|
23
|
+
end
|
24
|
+
|
25
|
+
def validate_secure_config!
|
26
|
+
validate_hash_or_boolean!(:secure)
|
27
|
+
validate_exclusive_use_of_hash_constraints!(config[:secure], :secure)
|
28
|
+
end
|
29
|
+
|
30
|
+
def validate_httponly_config!
|
31
|
+
validate_hash_or_boolean!(:httponly)
|
32
|
+
validate_exclusive_use_of_hash_constraints!(config[:httponly], :httponly)
|
33
|
+
end
|
34
|
+
|
35
|
+
def validate_samesite_config!
|
36
|
+
raise CookiesConfigError.new("samesite cookie config must be a hash") unless is_hash?(config[:samesite])
|
37
|
+
|
38
|
+
validate_samesite_boolean_config!
|
39
|
+
validate_samesite_hash_config!
|
40
|
+
end
|
41
|
+
|
42
|
+
# when configuring with booleans, only one enforcement is permitted
|
43
|
+
def validate_samesite_boolean_config!
|
44
|
+
if config[:samesite].key?(:lax) && config[:samesite][:lax].is_a?(TrueClass) && config[:samesite].key?(:strict)
|
45
|
+
raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax and strict enforcement is not permitted.")
|
46
|
+
elsif config[:samesite].key?(:strict) && config[:samesite][:strict].is_a?(TrueClass) && config[:samesite].key?(:lax)
|
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
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def validate_samesite_hash_config!
|
52
|
+
# validate Hash-based samesite configuration
|
53
|
+
if is_hash?(config[:samesite][:lax])
|
54
|
+
validate_exclusive_use_of_hash_constraints!(config[:samesite][:lax], 'samesite lax')
|
55
|
+
|
56
|
+
if is_hash?(config[:samesite][:strict])
|
57
|
+
validate_exclusive_use_of_hash_constraints!(config[:samesite][:strict], 'samesite strict')
|
58
|
+
validate_exclusive_use_of_samesite_enforcement!(:only)
|
59
|
+
validate_exclusive_use_of_samesite_enforcement!(:except)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def validate_hash_or_boolean!(attribute)
|
65
|
+
if !(is_hash?(config[attribute]) || is_boolean?(config[attribute]))
|
66
|
+
raise CookiesConfigError.new("#{attribute} cookie config must be a hash or boolean")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# validate exclusive use of only or except but not both at the same time
|
71
|
+
def validate_exclusive_use_of_hash_constraints!(conf, attribute)
|
72
|
+
return unless is_hash?(conf)
|
73
|
+
|
74
|
+
if conf.key?(:only) && conf.key?(:except)
|
75
|
+
raise CookiesConfigError.new("#{attribute} cookie config is invalid, simultaneous use of conditional arguments `only` and `except` is not permitted.")
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# validate exclusivity of only and except members within strict and lax
|
80
|
+
def validate_exclusive_use_of_samesite_enforcement!(attribute)
|
81
|
+
if (intersection = (config[:samesite][:lax].fetch(attribute, []) & config[:samesite][:strict].fetch(attribute, []))).any?
|
82
|
+
raise CookiesConfigError.new("samesite cookie config is invalid, cookie(s) #{intersection.join(', ')} cannot be enforced as lax and strict")
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def is_hash?(obj)
|
87
|
+
obj && obj.is_a?(Hash)
|
88
|
+
end
|
89
|
+
|
90
|
+
def is_boolean?(obj)
|
91
|
+
obj && (obj.is_a?(TrueClass) || obj.is_a?(FalseClass))
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|