secure_headers 3.1.2 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +126 -0
- data/README.md +147 -24
- data/lib/secure_headers/configuration.rb +46 -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 +2 -0
- data/lib/secure_headers/headers/referrer_policy.rb +33 -0
- data/lib/secure_headers/middleware.rb +28 -8
- data/lib/secure_headers/railtie.rb +5 -1
- data/lib/secure_headers/utils/cookies_config.rb +94 -0
- data/lib/secure_headers/view_helper.rb +64 -0
- data/lib/secure_headers.rb +4 -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/content_security_policy_spec.rb +5 -0
- data/spec/lib/secure_headers/headers/cookie_spec.rb +164 -0
- data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +54 -0
- data/spec/lib/secure_headers/middleware_spec.rb +64 -13
- data/spec/lib/secure_headers/view_helpers_spec.rb +125 -0
- data/spec/lib/secure_headers_spec.rb +23 -0
- data/spec/spec_helper.rb +14 -0
- data/upgrading-to-3-0.md +1 -0
- metadata +13 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c3acd5dfb3fc3a7c4037ef42b72765a4f1b34ede
|
|
4
|
+
data.tar.gz: 98f0ce842bac69bf7c5f4d3d4cc19e4281ebe00e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 75d3b9033b09f0ac40675e8e8706185d9c0ca465a38f857228bf56857f5f44ba1eab5559b9c9f64f34ee398226ccd9dda5e26923f5325dcadd7be8ff0a957314
|
|
7
|
+
data.tar.gz: f6ffb2babf72009e60a4d4663ee6e933db8f8acb7159021e66a07168e1c6743df621309fb63e884f3257217d28951b196b2a192739860ce459f4fbabbab35622
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,129 @@
|
|
|
1
|
+
## 3.3.0 referrer-policy support
|
|
2
|
+
|
|
3
|
+
While not officially part of the spec and not implemented anywhere, support for the experimental [`referrer-policy` header](https://w3c.github.io/webappsec-referrer-policy/#referrer-policy-header) was [preemptively added](https://github.com/twitter/secureheaders/pull/249).
|
|
4
|
+
|
|
5
|
+
Additionally, two minor enhancements were added this version:
|
|
6
|
+
1. [Warn when the HPKP report host is the same as the current host](https://github.com/twitter/secureheaders/pull/246). By definition any generated reports would be reporting to a known compromised connection.
|
|
7
|
+
1. [Filter unsupported CSP directives when using Edge](https://github.com/twitter/secureheaders/pull/247). Previously, this was causing many warnings in the developer console.
|
|
8
|
+
|
|
9
|
+
## 3.2.0 Cookie settings and CSP hash sources
|
|
10
|
+
|
|
11
|
+
### Cookies
|
|
12
|
+
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
__Note__: Regardless of the configuration specified, Secure cookies are only enabled for HTTPS requests.
|
|
16
|
+
|
|
17
|
+
#### Boolean-based configuration
|
|
18
|
+
|
|
19
|
+
Boolean-based configuration is intended to globally enable or disable a specific cookie attribute.
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
config.cookies = {
|
|
23
|
+
secure: true, # mark all cookies as Secure
|
|
24
|
+
httponly: false, # do not mark any cookies as HttpOnly
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
#### Hash-based configuration
|
|
29
|
+
|
|
30
|
+
Hash-based configuration allows for fine-grained control.
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
config.cookies = {
|
|
34
|
+
secure: { except: ['_guest'] }, # mark all but the `_guest` cookie as Secure
|
|
35
|
+
httponly: { only: ['_rails_session'] }, # only mark the `_rails_session` cookie as HttpOnly
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
#### SameSite cookie configuration
|
|
40
|
+
|
|
41
|
+
SameSite cookies permit either `Strict` or `Lax` enforcement mode options.
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
config.cookies = {
|
|
45
|
+
samesite: {
|
|
46
|
+
strict: true # mark all cookies as SameSite=Strict
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`Strict` and `Lax` enforcement modes can also be specified using a Hash.
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
config.cookies = {
|
|
55
|
+
samesite: {
|
|
56
|
+
strict: { only: ['_rails_session'] },
|
|
57
|
+
lax: { only: ['_guest'] }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
#### Hash
|
|
63
|
+
|
|
64
|
+
`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`.
|
|
65
|
+
|
|
66
|
+
You can add hash sources directly to your policy :
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
::SecureHeaders::Configuration.default do |config|
|
|
70
|
+
config.csp = {
|
|
71
|
+
default_src: %w('self')
|
|
72
|
+
|
|
73
|
+
# this is a made up value but browsers will show the expected hash in the console.
|
|
74
|
+
script_src: %w(sha256-123456)
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
You can also use the automated inline script detection/collection/computation of hash source values in your app.
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
rake secure_headers:generate_hashes
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
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.
|
|
86
|
+
|
|
87
|
+
```yaml
|
|
88
|
+
---
|
|
89
|
+
scripts:
|
|
90
|
+
app/views/asdfs/index.html.erb:
|
|
91
|
+
- "'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg='"
|
|
92
|
+
styles:
|
|
93
|
+
app/views/asdfs/index.html.erb:
|
|
94
|
+
- "'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY='"
|
|
95
|
+
- "'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE='"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
##### Helpers
|
|
99
|
+
|
|
100
|
+
**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.
|
|
101
|
+
|
|
102
|
+
```erb
|
|
103
|
+
<%= hashed_style_tag do %>
|
|
104
|
+
body {
|
|
105
|
+
background-color: black;
|
|
106
|
+
}
|
|
107
|
+
<% end %>
|
|
108
|
+
|
|
109
|
+
<%= hashed_style_tag do %>
|
|
110
|
+
body {
|
|
111
|
+
font-size: 30px;
|
|
112
|
+
font-color: green;
|
|
113
|
+
}
|
|
114
|
+
<% end %>
|
|
115
|
+
|
|
116
|
+
<%= hashed_javascript_tag do %>
|
|
117
|
+
console.log(1)
|
|
118
|
+
<% end %>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
Content-Security-Policy: ...
|
|
123
|
+
script-src 'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg=' ... ;
|
|
124
|
+
style-src 'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY=' 'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE=' ...;
|
|
125
|
+
```
|
|
126
|
+
|
|
1
127
|
## 3.1.2 Bug fix for regression
|
|
2
128
|
|
|
3
129
|
See https://github.com/twitter/secureheaders/pull/239
|
data/README.md
CHANGED
|
@@ -13,9 +13,10 @@ The gem will automatically apply several headers that are related to security.
|
|
|
13
13
|
- X-Content-Type-Options - [Prevent content type sniffing](https://msdn.microsoft.com/library/gg622941\(v=vs.85\).aspx)
|
|
14
14
|
- X-Download-Options - [Prevent file downloads opening](https://msdn.microsoft.com/library/jj542450(v=vs.85).aspx)
|
|
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
|
+
- Referrer-Policy - [Referrer Policy draft](https://w3c.github.io/webappsec-referrer-policy/)
|
|
16
17
|
- 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
18
|
|
|
18
|
-
It can also mark all http cookies with the
|
|
19
|
+
It can also mark all http cookies with the Secure, HttpOnly and SameSite attributes (when configured to do so).
|
|
19
20
|
|
|
20
21
|
`secure_headers` is a library with a global config, per request overrides, and rack middleware that enables you customize your application settings.
|
|
21
22
|
|
|
@@ -31,13 +32,20 @@ All `nil` values will fallback to their default values. `SecureHeaders::OPT_OUT`
|
|
|
31
32
|
|
|
32
33
|
```ruby
|
|
33
34
|
SecureHeaders::Configuration.default do |config|
|
|
34
|
-
config.
|
|
35
|
+
config.cookies = {
|
|
36
|
+
secure: true, # mark all cookies as "Secure"
|
|
37
|
+
httponly: true, # mark all cookies as "HttpOnly"
|
|
38
|
+
samesite: {
|
|
39
|
+
strict: true # mark all cookies as SameSite=Strict
|
|
40
|
+
}
|
|
41
|
+
}
|
|
35
42
|
config.hsts = "max-age=#{20.years.to_i}; includeSubdomains; preload"
|
|
36
43
|
config.x_frame_options = "DENY"
|
|
37
44
|
config.x_content_type_options = "nosniff"
|
|
38
45
|
config.x_xss_protection = "1; mode=block"
|
|
39
46
|
config.x_download_options = "noopen"
|
|
40
47
|
config.x_permitted_cross_domain_policies = "none"
|
|
48
|
+
config.referrer_policy = "origin-when-cross-origin"
|
|
41
49
|
config.csp = {
|
|
42
50
|
# "meta" values. these will shaped the header, but the values are not included in the header.
|
|
43
51
|
report_only: true, # default: false
|
|
@@ -195,24 +203,6 @@ Code | Result
|
|
|
195
203
|
|
|
196
204
|
#### Nonce
|
|
197
205
|
|
|
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
206
|
You can use a view helper to automatically add nonces to script tags:
|
|
217
207
|
|
|
218
208
|
```erb
|
|
@@ -240,9 +230,93 @@ body {
|
|
|
240
230
|
</style>
|
|
241
231
|
```
|
|
242
232
|
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Content-Security-Policy: ...
|
|
236
|
+
script-src 'nonce-/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=' ...;
|
|
237
|
+
style-src 'nonce-/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=' ...;
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
`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.
|
|
241
|
+
|
|
242
|
+
```erb
|
|
243
|
+
<script nonce="<%= content_security_policy_script_nonce %>">
|
|
244
|
+
console.log("whitelisted, will execute")
|
|
245
|
+
</script>
|
|
246
|
+
|
|
247
|
+
<script nonce="lol">
|
|
248
|
+
console.log("won't execute, not whitelisted")
|
|
249
|
+
</script>
|
|
250
|
+
|
|
251
|
+
<script>
|
|
252
|
+
console.log("won't execute, not whitelisted")
|
|
253
|
+
</script>
|
|
254
|
+
```
|
|
255
|
+
|
|
243
256
|
#### Hash
|
|
244
257
|
|
|
245
|
-
|
|
258
|
+
`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`.
|
|
259
|
+
|
|
260
|
+
You can add hash sources directly to your policy :
|
|
261
|
+
|
|
262
|
+
```ruby
|
|
263
|
+
::SecureHeaders::Configuration.default do |config|
|
|
264
|
+
config.csp = {
|
|
265
|
+
default_src: %w('self')
|
|
266
|
+
|
|
267
|
+
# this is a made up value but browsers will show the expected hash in the console.
|
|
268
|
+
script_src: %w(sha256-123456)
|
|
269
|
+
}
|
|
270
|
+
end
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
You can also use the automated inline script detection/collection/computation of hash source values in your app.
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
rake secure_headers:generate_hashes
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
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.
|
|
280
|
+
|
|
281
|
+
```yaml
|
|
282
|
+
---
|
|
283
|
+
scripts:
|
|
284
|
+
app/views/asdfs/index.html.erb:
|
|
285
|
+
- "'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg='"
|
|
286
|
+
styles:
|
|
287
|
+
app/views/asdfs/index.html.erb:
|
|
288
|
+
- "'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY='"
|
|
289
|
+
- "'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE='"
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
##### Helpers
|
|
293
|
+
|
|
294
|
+
**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.
|
|
295
|
+
|
|
296
|
+
```erb
|
|
297
|
+
<%= hashed_style_tag do %>
|
|
298
|
+
body {
|
|
299
|
+
background-color: black;
|
|
300
|
+
}
|
|
301
|
+
<% end %>
|
|
302
|
+
|
|
303
|
+
<%= hashed_style_tag do %>
|
|
304
|
+
body {
|
|
305
|
+
font-size: 30px;
|
|
306
|
+
font-color: green;
|
|
307
|
+
}
|
|
308
|
+
<% end %>
|
|
309
|
+
|
|
310
|
+
<%= hashed_javascript_tag do %>
|
|
311
|
+
console.log(1)
|
|
312
|
+
<% end %>
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
```
|
|
316
|
+
Content-Security-Policy: ...
|
|
317
|
+
script-src 'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg=' ... ;
|
|
318
|
+
style-src 'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY=' 'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE=' ...;
|
|
319
|
+
```
|
|
246
320
|
|
|
247
321
|
### Public Key Pins
|
|
248
322
|
|
|
@@ -258,9 +332,58 @@ config.hpkp = {
|
|
|
258
332
|
{sha256: '73a2c64f9545172c1195efb6616ca5f7afd1df6f245407cafb90de3998a1c97f'}
|
|
259
333
|
],
|
|
260
334
|
report_only: true, # defaults to false (report-only mode)
|
|
261
|
-
report_uri: 'https://report-uri.io/example-hpkp'
|
|
262
|
-
|
|
263
|
-
|
|
335
|
+
report_uri: 'https://report-uri.io/example-hpkp'
|
|
336
|
+
}
|
|
337
|
+
```
|
|
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
|
+
}
|
|
264
387
|
}
|
|
265
388
|
```
|
|
266
389
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require 'yaml'
|
|
2
|
+
|
|
1
3
|
module SecureHeaders
|
|
2
4
|
class Configuration
|
|
3
5
|
DEFAULT_CONFIG = :default
|
|
@@ -59,6 +61,7 @@ module SecureHeaders
|
|
|
59
61
|
config.validate_config!
|
|
60
62
|
@configurations ||= {}
|
|
61
63
|
config.send(:cache_headers!)
|
|
64
|
+
config.send(:cache_hpkp_report_host)
|
|
62
65
|
config.freeze
|
|
63
66
|
@configurations[name] = config
|
|
64
67
|
end
|
|
@@ -100,14 +103,24 @@ module SecureHeaders
|
|
|
100
103
|
end
|
|
101
104
|
end
|
|
102
105
|
|
|
106
|
+
attr_accessor :dynamic_csp
|
|
107
|
+
|
|
103
108
|
attr_writer :hsts, :x_frame_options, :x_content_type_options,
|
|
104
109
|
:x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies,
|
|
105
|
-
:
|
|
110
|
+
:referrer_policy
|
|
106
111
|
|
|
107
|
-
attr_reader :cached_headers, :csp, :
|
|
112
|
+
attr_reader :cached_headers, :csp, :cookies, :hpkp, :hpkp_report_host
|
|
113
|
+
|
|
114
|
+
HASH_CONFIG_FILE = ENV["secure_headers_generated_hashes_file"] || "config/secure_headers_generated_hashes.yml"
|
|
115
|
+
if File.exists?(HASH_CONFIG_FILE)
|
|
116
|
+
config = YAML.safe_load(File.open(HASH_CONFIG_FILE))
|
|
117
|
+
@script_hashes = config["scripts"]
|
|
118
|
+
@style_hashes = config["styles"]
|
|
119
|
+
end
|
|
108
120
|
|
|
109
121
|
def initialize(&block)
|
|
110
122
|
self.hpkp = OPT_OUT
|
|
123
|
+
self.referrer_policy = OPT_OUT
|
|
111
124
|
self.csp = self.class.send(:deep_copy, CSP::DEFAULT_CONFIG)
|
|
112
125
|
instance_eval &block if block_given?
|
|
113
126
|
end
|
|
@@ -117,7 +130,7 @@ module SecureHeaders
|
|
|
117
130
|
# Returns a deep-dup'd copy of this configuration.
|
|
118
131
|
def dup
|
|
119
132
|
copy = self.class.new
|
|
120
|
-
copy.
|
|
133
|
+
copy.cookies = @cookies
|
|
121
134
|
copy.csp = self.class.send(:deep_copy_if_hash, @csp)
|
|
122
135
|
copy.dynamic_csp = self.class.send(:deep_copy_if_hash, @dynamic_csp)
|
|
123
136
|
copy.cached_headers = self.class.send(:deep_copy_if_hash, @cached_headers)
|
|
@@ -127,7 +140,9 @@ module SecureHeaders
|
|
|
127
140
|
copy.x_xss_protection = @x_xss_protection
|
|
128
141
|
copy.x_download_options = @x_download_options
|
|
129
142
|
copy.x_permitted_cross_domain_policies = @x_permitted_cross_domain_policies
|
|
143
|
+
copy.referrer_policy = @referrer_policy
|
|
130
144
|
copy.hpkp = @hpkp
|
|
145
|
+
copy.hpkp_report_host = @hpkp_report_host
|
|
131
146
|
copy
|
|
132
147
|
end
|
|
133
148
|
|
|
@@ -166,30 +181,57 @@ module SecureHeaders
|
|
|
166
181
|
def validate_config!
|
|
167
182
|
StrictTransportSecurity.validate_config!(@hsts)
|
|
168
183
|
ContentSecurityPolicy.validate_config!(@csp)
|
|
184
|
+
ReferrerPolicy.validate_config!(@referrer_policy)
|
|
169
185
|
XFrameOptions.validate_config!(@x_frame_options)
|
|
170
186
|
XContentTypeOptions.validate_config!(@x_content_type_options)
|
|
171
187
|
XXssProtection.validate_config!(@x_xss_protection)
|
|
172
188
|
XDownloadOptions.validate_config!(@x_download_options)
|
|
173
189
|
XPermittedCrossDomainPolicies.validate_config!(@x_permitted_cross_domain_policies)
|
|
174
190
|
PublicKeyPins.validate_config!(@hpkp)
|
|
191
|
+
Cookie.validate_config!(@cookies)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def secure_cookies=(secure_cookies)
|
|
195
|
+
Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#secure_cookies=` is deprecated. Please use `#cookies=` to configure secure cookies instead."
|
|
196
|
+
@cookies = (@cookies || {}).merge(secure: secure_cookies)
|
|
175
197
|
end
|
|
176
198
|
|
|
177
199
|
protected
|
|
178
200
|
|
|
179
201
|
def csp=(new_csp)
|
|
180
202
|
if self.dynamic_csp
|
|
181
|
-
raise IllegalPolicyModificationError, "You are attempting to modify CSP settings directly. Use dynamic_csp=
|
|
203
|
+
raise IllegalPolicyModificationError, "You are attempting to modify CSP settings directly. Use dynamic_csp= instead."
|
|
182
204
|
end
|
|
183
205
|
|
|
184
206
|
@csp = new_csp
|
|
185
207
|
end
|
|
186
208
|
|
|
209
|
+
def cookies=(cookies)
|
|
210
|
+
@cookies = cookies
|
|
211
|
+
end
|
|
212
|
+
|
|
187
213
|
def cached_headers=(headers)
|
|
188
214
|
@cached_headers = headers
|
|
189
215
|
end
|
|
190
216
|
|
|
217
|
+
def hpkp=(hpkp)
|
|
218
|
+
@hpkp = hpkp
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def hpkp_report_host=(hpkp_report_host)
|
|
222
|
+
@hpkp_report_host = hpkp_report_host
|
|
223
|
+
end
|
|
224
|
+
|
|
191
225
|
private
|
|
192
226
|
|
|
227
|
+
def cache_hpkp_report_host
|
|
228
|
+
has_report_uri = @hpkp && @hpkp != OPT_OUT && @hpkp[:report_uri]
|
|
229
|
+
self.hpkp_report_host = if has_report_uri
|
|
230
|
+
parsed_report_uri = URI.parse(@hpkp[:report_uri])
|
|
231
|
+
parsed_report_uri.host
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
193
235
|
# Public: Precompute the header names and values for this configuraiton.
|
|
194
236
|
# Ensures that headers generated at configure time, not on demand.
|
|
195
237
|
#
|
|
@@ -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
|
|
@@ -91,6 +91,7 @@ module SecureHeaders
|
|
|
91
91
|
UPGRADE_INSECURE_REQUESTS
|
|
92
92
|
].freeze
|
|
93
93
|
|
|
94
|
+
EDGE_DIRECTIVES = DIRECTIVES_1_0
|
|
94
95
|
SAFARI_DIRECTIVES = DIRECTIVES_1_0
|
|
95
96
|
|
|
96
97
|
FIREFOX_UNSUPPORTED_DIRECTIVES = [
|
|
@@ -118,6 +119,7 @@ module SecureHeaders
|
|
|
118
119
|
"Opera" => CHROME_DIRECTIVES,
|
|
119
120
|
"Firefox" => FIREFOX_DIRECTIVES,
|
|
120
121
|
"Safari" => SAFARI_DIRECTIVES,
|
|
122
|
+
"Edge" => EDGE_DIRECTIVES,
|
|
121
123
|
"Other" => CHROME_DIRECTIVES
|
|
122
124
|
}.freeze
|
|
123
125
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module SecureHeaders
|
|
2
|
+
class ReferrerPolicyConfigError < StandardError; end
|
|
3
|
+
class ReferrerPolicy
|
|
4
|
+
HEADER_NAME = "Referrer-Policy"
|
|
5
|
+
DEFAULT_VALUE = "origin-when-cross-origin"
|
|
6
|
+
VALID_POLICIES = %w(
|
|
7
|
+
no-referrer
|
|
8
|
+
no-referrer-when-downgrade
|
|
9
|
+
origin
|
|
10
|
+
origin-when-cross-origin
|
|
11
|
+
unsafe-url
|
|
12
|
+
)
|
|
13
|
+
CONFIG_KEY = :referrer_policy
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
# Public: generate an Referrer Policy header.
|
|
17
|
+
#
|
|
18
|
+
# Returns a default header if no configuration is provided, or a
|
|
19
|
+
# header name and value based on the config.
|
|
20
|
+
def make_header(config = nil)
|
|
21
|
+
[HEADER_NAME, config || DEFAULT_VALUE]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def validate_config!(config)
|
|
25
|
+
return if config.nil? || config == OPT_OUT
|
|
26
|
+
raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String)
|
|
27
|
+
unless VALID_POLICIES.include?(config.downcase)
|
|
28
|
+
raise ReferrerPolicyConfigError.new("Value can only be one of #{VALID_POLICIES.join(', ')}")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|