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 +4 -4
- data/CHANGELOG.md +133 -0
- data/README.md +145 -19
- data/lib/secure_headers/configuration.rb +28 -4
- data/lib/secure_headers/hash_helper.rb +10 -0
- data/lib/secure_headers/headers/cookie.rb +126 -0
- data/lib/secure_headers/headers/policy_management.rb +1 -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 +17 -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/lib/secure_headers_spec.rb +19 -1
- 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,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
|
-
|
|
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, :
|
|
107
|
+
:hpkp, :dynamic_csp, :cookies
|
|
108
|
+
|
|
109
|
+
attr_reader :cached_headers, :csp, :dynamic_csp, :cookies
|
|
105
110
|
|
|
106
|
-
|
|
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.
|
|
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=
|
|
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
|
-
|
|
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
|