secure_headers 3.5.0 → 3.6.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 +9 -0
- data/README.md +34 -382
- data/docs/HPKP.md +17 -0
- data/docs/cookies.md +50 -0
- data/docs/hashes.md +64 -0
- data/docs/named_overrides_and_appends.md +107 -0
- data/docs/per_action_configuration.md +105 -0
- data/docs/sinatra.md +25 -0
- data/lib/secure_headers/configuration.rb +3 -1
- data/lib/secure_headers/headers/clear_site_data.rb +51 -0
- data/lib/secure_headers/headers/content_security_policy.rb +2 -1
- data/lib/secure_headers/headers/policy_management.rb +2 -1
- data/lib/secure_headers/headers/referrer_policy.rb +3 -0
- data/lib/secure_headers.rb +2 -0
- data/secure_headers.gemspec +1 -1
- data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +103 -0
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +12 -0
- data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +18 -0
- data/spec/lib/secure_headers_spec.rb +8 -0
- metadata +11 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7585a939ce1ded2d2226ba1d1503a5f0dca3eb49
|
|
4
|
+
data.tar.gz: 3762611d81eb31f24cdfd04a609d7aaed5a62182
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4ba111dea43dd24b25567ff18e98fb1de8ad5f5a04566a9390e9a20e9c405571038960bd3dca417d225f58deea82214e27d8381448c887607db2883cd71b6dda
|
|
7
|
+
data.tar.gz: 7dd6cf414c1e3ed0814b5095730cd98ee77259c3560dc7a9371754dbbbff837f44a7cc93d664f85d7317fb6f633f0b2a5d8145f7d7fac5833a5dfc83fa0b4a72
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
## 3.6.0
|
|
2
|
+
|
|
3
|
+
Add support for the clear-site-data header
|
|
4
|
+
|
|
5
|
+
## 3.5.1
|
|
6
|
+
|
|
7
|
+
* Fix bug that can occur when useragent library version is older, resulting in a nil version sometimes.
|
|
8
|
+
* Add constant for `strict-dynamic`
|
|
9
|
+
|
|
1
10
|
## 3.5.0
|
|
2
11
|
|
|
3
12
|
This release adds support for setting two CSP headers (enforced/report-only) and management around them.
|
data/README.md
CHANGED
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
|
|
8
8
|
The gem will automatically apply several headers that are related to security. This includes:
|
|
9
9
|
- Content Security Policy (CSP) - Helps detect/prevent XSS, mixed-content, and other classes of attack. [CSP 2 Specification](http://www.w3.org/TR/CSP2/)
|
|
10
|
+
- https://csp.withgoogle.com
|
|
11
|
+
- https://csp.withgoogle.com/docs/strict-csp.html
|
|
12
|
+
- https://csp-evaluator.withgoogle.com
|
|
10
13
|
- HTTP Strict Transport Security (HSTS) - Ensures the browser never visits the http version of a website. Protects from SSLStrip/Firesheep attacks. [HSTS Specification](https://tools.ietf.org/html/rfc6797)
|
|
11
14
|
- X-Frame-Options (XFO) - Prevents your content from being framed and potentially clickjacked. [X-Frame-Options Specification](https://tools.ietf.org/html/rfc7034)
|
|
12
15
|
- X-XSS-Protection - [Cross site scripting heuristic filter for IE/Chrome](https://msdn.microsoft.com/en-us/library/dd565647\(v=vs.85\).aspx)
|
|
@@ -15,11 +18,35 @@ The gem will automatically apply several headers that are related to security.
|
|
|
15
18
|
- 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
19
|
- Referrer-Policy - [Referrer Policy draft](https://w3c.github.io/webappsec-referrer-policy/)
|
|
17
20
|
- 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)
|
|
21
|
+
- Clear-Site-Data - Clearing browser data for origin. [Clear-Site-Data specification](https://www.w3.org/TR/clear-site-data/).
|
|
18
22
|
|
|
19
23
|
It can also mark all http cookies with the Secure, HttpOnly and SameSite attributes (when configured to do so).
|
|
20
24
|
|
|
21
25
|
`secure_headers` is a library with a global config, per request overrides, and rack middleware that enables you customize your application settings.
|
|
22
26
|
|
|
27
|
+
## Documentation
|
|
28
|
+
|
|
29
|
+
- [Named overrides and appends](docs/named_overrides_and_appends.md)
|
|
30
|
+
- [Per action configuration](docs/per_action_configuration.md)
|
|
31
|
+
- [Cookies](docs/cookies.md)
|
|
32
|
+
- [HPKP](docs/HPKP.md)
|
|
33
|
+
- [Hashes](docs/hashes.md)
|
|
34
|
+
- [Sinatra Config](docs/sinatra.md)
|
|
35
|
+
|
|
36
|
+
## Getting Started
|
|
37
|
+
|
|
38
|
+
### Rails 3+
|
|
39
|
+
|
|
40
|
+
For Rails 3+ applications, `secure_headers` has a `railtie` that should automatically include the middleware. If for some reason the middleware is not being included follow the instructions for Rails 2.
|
|
41
|
+
|
|
42
|
+
### Rails 2
|
|
43
|
+
|
|
44
|
+
For Rails 2 or non-rails applications, an explicit statement is required to use the middleware component.
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
use SecureHeaders::Middleware
|
|
48
|
+
```
|
|
49
|
+
|
|
23
50
|
## Configuration
|
|
24
51
|
|
|
25
52
|
If you do not supply a `default` configuration, exceptions will be raised. If you would like to use a default configuration (which is fairly locked down), just call `SecureHeaders::Configuration.default` without any arguments or block.
|
|
@@ -42,6 +69,12 @@ SecureHeaders::Configuration.default do |config|
|
|
|
42
69
|
config.x_download_options = "noopen"
|
|
43
70
|
config.x_permitted_cross_domain_policies = "none"
|
|
44
71
|
config.referrer_policy = "origin-when-cross-origin"
|
|
72
|
+
config.clear_site_data = [
|
|
73
|
+
"cache",
|
|
74
|
+
"cookies",
|
|
75
|
+
"storage",
|
|
76
|
+
"executionContexts"
|
|
77
|
+
]
|
|
45
78
|
config.csp = {
|
|
46
79
|
# "meta" values. these will shaped the header, but the values are not included in the header.
|
|
47
80
|
report_only: true, # default: false [DEPRECATED from 3.5.0: instead, configure csp_report_only]
|
|
@@ -83,17 +116,9 @@ SecureHeaders::Configuration.default do |config|
|
|
|
83
116
|
end
|
|
84
117
|
```
|
|
85
118
|
|
|
86
|
-
### rails 2
|
|
87
|
-
|
|
88
|
-
For rails 3+ applications, `secure_headers` has a `railtie` that should automatically include the middleware. For rails 2 or non-rails applications, an explicit statement is required to use the middleware component.
|
|
89
|
-
|
|
90
|
-
```ruby
|
|
91
|
-
use SecureHeaders::Middleware
|
|
92
|
-
```
|
|
93
|
-
|
|
94
119
|
## Default values
|
|
95
120
|
|
|
96
|
-
All headers except for PublicKeyPins have a default value.
|
|
121
|
+
All headers except for PublicKeyPins and ClearSiteData have a default value. The default set of headers is:
|
|
97
122
|
|
|
98
123
|
```
|
|
99
124
|
Content-Security-Policy: default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'
|
|
@@ -118,379 +143,6 @@ Configuration.default do |config|
|
|
|
118
143
|
end
|
|
119
144
|
```
|
|
120
145
|
|
|
121
|
-
## Named Appends
|
|
122
|
-
|
|
123
|
-
Named Appends are blocks of code that can be reused and composed during requests. e.g. If a certain partial is rendered conditionally, and the csp needs to be adjusted for that partial, you can create a named append for that situation. The value returned by the block will be passed into `append_content_security_policy_directives`. The current request object is passed as an argument to the block for even more flexibility.
|
|
124
|
-
|
|
125
|
-
```ruby
|
|
126
|
-
def show
|
|
127
|
-
if include_widget?
|
|
128
|
-
@widget = widget.render
|
|
129
|
-
use_content_security_policy_named_append(:widget_partial)
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
SecureHeaders::Configuration.named_append(:widget_partial) do |request|
|
|
135
|
-
SecureHeaders.override_x_frame_options(request, "DENY")
|
|
136
|
-
if request.controller_instance.current_user.in_test_bucket?
|
|
137
|
-
{ child_src: %w(beta.thirdpartyhost.com) }
|
|
138
|
-
else
|
|
139
|
-
{ child_src: %w(thirdpartyhost.com) }
|
|
140
|
-
end
|
|
141
|
-
end
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
You can use as many named appends as you would like per request, but be careful because order of inclusion matters. Consider the following:
|
|
145
|
-
|
|
146
|
-
```ruby
|
|
147
|
-
SecureHeader::Configuration.default do |config|
|
|
148
|
-
config.csp = { default_src: %w('self')}
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
SecureHeaders::Configuration.named_append(:A) do |request|
|
|
152
|
-
{ default_src: %w(myhost.com) }
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
SecureHeaders::Configuration.named_append(:B) do |request|
|
|
156
|
-
{ script_src: %w('unsafe-eval') }
|
|
157
|
-
end
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
The following code will produce different policies due to the way policies are normalized (e.g. providing a previously undefined directive that inherits from `default-src`, removing host source values when `*` is provided. Removing `'none'` when additional values are present, etc.):
|
|
161
|
-
|
|
162
|
-
```ruby
|
|
163
|
-
def index
|
|
164
|
-
use_content_security_policy_named_append(:A)
|
|
165
|
-
use_content_security_policy_named_append(:B)
|
|
166
|
-
# produces default-src 'self' myhost.com; script-src 'self' myhost.com 'unsafe-eval';
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
def show
|
|
170
|
-
use_content_security_policy_named_append(:B)
|
|
171
|
-
use_content_security_policy_named_append(:A)
|
|
172
|
-
# produces default-src 'self' myhost.com; script-src 'self' 'unsafe-eval';
|
|
173
|
-
end
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
## Named overrides
|
|
178
|
-
|
|
179
|
-
Named overrides serve two purposes:
|
|
180
|
-
|
|
181
|
-
* To be able to refer to a configuration by simple name.
|
|
182
|
-
* By precomputing the headers for a named configuration, the headers generated once and reused over every request.
|
|
183
|
-
|
|
184
|
-
To use a named override, drop a `SecureHeaders::Configuration.override` block **outside** of method definitions and then declare which named override you'd like to use. You can even override an override.
|
|
185
|
-
|
|
186
|
-
```ruby
|
|
187
|
-
class ApplicationController < ActionController::Base
|
|
188
|
-
SecureHeaders::Configuration.default do |config|
|
|
189
|
-
config.csp = {
|
|
190
|
-
default_src: %w('self'),
|
|
191
|
-
script_src: %w(example.org)
|
|
192
|
-
}
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
# override default configuration
|
|
196
|
-
SecureHeaders::Configuration.override(:script_from_otherdomain_com) do |config|
|
|
197
|
-
config.csp[:script_src] << "otherdomain.com"
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
# overrides the :script_from_otherdomain_com configuration
|
|
201
|
-
SecureHeaders::Configuration.override(:another_config, :script_from_otherdomain_com) do |config|
|
|
202
|
-
config.csp[:script_src] << "evenanotherdomain.com"
|
|
203
|
-
end
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
class MyController < ApplicationController
|
|
207
|
-
def index
|
|
208
|
-
# Produces default-src 'self'; script-src example.org otherdomain.com
|
|
209
|
-
use_secure_headers_override(:script_from_otherdomain_com)
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
def show
|
|
213
|
-
# Produces default-src 'self'; script-src example.org otherdomain.org evenanotherdomain.com
|
|
214
|
-
use_secure_headers_override(:another_config)
|
|
215
|
-
end
|
|
216
|
-
end
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
By default, a no-op configuration is provided. No headers will be set when this default override is used.
|
|
220
|
-
|
|
221
|
-
```ruby
|
|
222
|
-
class MyController < ApplicationController
|
|
223
|
-
def index
|
|
224
|
-
SecureHeaders.opt_out_of_all_protection(request)
|
|
225
|
-
end
|
|
226
|
-
end
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
## Per-action configuration
|
|
230
|
-
|
|
231
|
-
You can override the settings for a given action by producing a temporary override. Be aware that because of the dynamic nature of the value, the header values will be computed per request.
|
|
232
|
-
|
|
233
|
-
```ruby
|
|
234
|
-
# Given a config of:
|
|
235
|
-
::SecureHeaders::Configuration.default do |config|
|
|
236
|
-
config.csp = {
|
|
237
|
-
default_src: %w('self'),
|
|
238
|
-
script_src: %w('self')
|
|
239
|
-
}
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
class MyController < ApplicationController
|
|
243
|
-
def index
|
|
244
|
-
# Append value to the source list, override 'none' values
|
|
245
|
-
# Produces: default-src 'self'; script-src 'self' s3.amazonaws.com; object-src 'self' www.youtube.com
|
|
246
|
-
append_content_security_policy_directives(script_src: %w(s3.amazonaws.com), object_src: %w('self' www.youtube.com))
|
|
247
|
-
|
|
248
|
-
# Overrides the previously set source list, override 'none' values
|
|
249
|
-
# Produces: default-src 'self'; script-src s3.amazonaws.com; object-src 'self'
|
|
250
|
-
override_content_security_policy_directives(script_src: %w(s3.amazonaws.com), object_src: %w('self'))
|
|
251
|
-
|
|
252
|
-
# Global settings default to "sameorigin"
|
|
253
|
-
override_x_frame_options("DENY")
|
|
254
|
-
end
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
The following methods are available as controller instance methods. They are also available as class methods, but require you to pass in the `request` object.
|
|
258
|
-
* `append_content_security_policy_directives(hash)`: appends each value to the corresponding CSP app-wide configuration.
|
|
259
|
-
* `override_content_security_policy_directives(hash)`: merges the hash into the app-wide configuration, overwriting any previous config
|
|
260
|
-
* `override_x_frame_options(value)`: sets the `X-Frame-Options header` to `value`
|
|
261
|
-
|
|
262
|
-
## Appending / overriding Content Security Policy
|
|
263
|
-
|
|
264
|
-
When manipulating content security policy, there are a few things to consider. The default header value is `default-src https:` which corresponds to a default configuration of `{ default_src: %w(https:)}`.
|
|
265
|
-
|
|
266
|
-
#### Append to the policy with a directive other than `default_src`
|
|
267
|
-
|
|
268
|
-
The value of `default_src` is joined with the addition if the it is a [fetch directive](https://w3c.github.io/webappsec-csp/#directives-fetch). Note the `https:` is carried over from the `default-src` config. If you do not want this, use `override_content_security_policy_directives` instead. To illustrate:
|
|
269
|
-
|
|
270
|
-
```ruby
|
|
271
|
-
::SecureHeaders::Configuration.default do |config|
|
|
272
|
-
config.csp = {
|
|
273
|
-
default_src: %w('self')
|
|
274
|
-
}
|
|
275
|
-
end
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
Code | Result
|
|
279
|
-
------------- | -------------
|
|
280
|
-
`append_content_security_policy_directives(script_src: %w(mycdn.com))` | `default-src 'self'; script-src 'self' mycdn.com`
|
|
281
|
-
`override_content_security_policy_directives(script_src: %w(mycdn.com))` | `default-src 'self'; script-src mycdn.com`
|
|
282
|
-
|
|
283
|
-
#### Nonce
|
|
284
|
-
|
|
285
|
-
You can use a view helper to automatically add nonces to script tags:
|
|
286
|
-
|
|
287
|
-
```erb
|
|
288
|
-
<%= nonced_javascript_tag do %>
|
|
289
|
-
console.log("nonced!");
|
|
290
|
-
<% end %>
|
|
291
|
-
|
|
292
|
-
<%= nonced_style_tag do %>
|
|
293
|
-
body {
|
|
294
|
-
background-color: black;
|
|
295
|
-
}
|
|
296
|
-
<% end %>
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
becomes:
|
|
300
|
-
|
|
301
|
-
```html
|
|
302
|
-
<script nonce="/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=">
|
|
303
|
-
console.log("nonced!")
|
|
304
|
-
</script>
|
|
305
|
-
<style nonce="/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=">
|
|
306
|
-
body {
|
|
307
|
-
background-color: black;
|
|
308
|
-
}
|
|
309
|
-
</style>
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
Content-Security-Policy: ...
|
|
315
|
-
script-src 'nonce-/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=' ...;
|
|
316
|
-
style-src 'nonce-/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=' ...;
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
`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.
|
|
320
|
-
|
|
321
|
-
```erb
|
|
322
|
-
<script nonce="<%= content_security_policy_script_nonce %>">
|
|
323
|
-
console.log("whitelisted, will execute")
|
|
324
|
-
</script>
|
|
325
|
-
|
|
326
|
-
<script nonce="lol">
|
|
327
|
-
console.log("won't execute, not whitelisted")
|
|
328
|
-
</script>
|
|
329
|
-
|
|
330
|
-
<script>
|
|
331
|
-
console.log("won't execute, not whitelisted")
|
|
332
|
-
</script>
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
#### Hash
|
|
336
|
-
|
|
337
|
-
`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`.
|
|
338
|
-
|
|
339
|
-
You can add hash sources directly to your policy :
|
|
340
|
-
|
|
341
|
-
```ruby
|
|
342
|
-
::SecureHeaders::Configuration.default do |config|
|
|
343
|
-
config.csp = {
|
|
344
|
-
default_src: %w('self')
|
|
345
|
-
|
|
346
|
-
# this is a made up value but browsers will show the expected hash in the console.
|
|
347
|
-
script_src: %w(sha256-123456)
|
|
348
|
-
}
|
|
349
|
-
end
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
You can also use the automated inline script detection/collection/computation of hash source values in your app.
|
|
353
|
-
|
|
354
|
-
```bash
|
|
355
|
-
rake secure_headers:generate_hashes
|
|
356
|
-
```
|
|
357
|
-
|
|
358
|
-
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.
|
|
359
|
-
|
|
360
|
-
```yaml
|
|
361
|
-
---
|
|
362
|
-
scripts:
|
|
363
|
-
app/views/asdfs/index.html.erb:
|
|
364
|
-
- "'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg='"
|
|
365
|
-
styles:
|
|
366
|
-
app/views/asdfs/index.html.erb:
|
|
367
|
-
- "'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY='"
|
|
368
|
-
- "'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE='"
|
|
369
|
-
```
|
|
370
|
-
|
|
371
|
-
##### Helpers
|
|
372
|
-
|
|
373
|
-
**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.
|
|
374
|
-
|
|
375
|
-
```erb
|
|
376
|
-
<%= hashed_style_tag do %>
|
|
377
|
-
body {
|
|
378
|
-
background-color: black;
|
|
379
|
-
}
|
|
380
|
-
<% end %>
|
|
381
|
-
|
|
382
|
-
<%= hashed_style_tag do %>
|
|
383
|
-
body {
|
|
384
|
-
font-size: 30px;
|
|
385
|
-
font-color: green;
|
|
386
|
-
}
|
|
387
|
-
<% end %>
|
|
388
|
-
|
|
389
|
-
<%= hashed_javascript_tag do %>
|
|
390
|
-
console.log(1)
|
|
391
|
-
<% end %>
|
|
392
|
-
```
|
|
393
|
-
|
|
394
|
-
```
|
|
395
|
-
Content-Security-Policy: ...
|
|
396
|
-
script-src 'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg=' ... ;
|
|
397
|
-
style-src 'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY=' 'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE=' ...;
|
|
398
|
-
```
|
|
399
|
-
|
|
400
|
-
### Public Key Pins
|
|
401
|
-
|
|
402
|
-
Be aware that pinning error reporting is governed by the same rules as everything else. If you have a pinning failure that tries to report back to the same origin, by definition this will not work.
|
|
403
|
-
|
|
404
|
-
```ruby
|
|
405
|
-
config.hpkp = {
|
|
406
|
-
max_age: 60.days.to_i, # max_age is a required parameter
|
|
407
|
-
include_subdomains: true, # whether or not to apply pins to subdomains
|
|
408
|
-
# Per the spec, SHA256 hashes are the only currently supported format.
|
|
409
|
-
pins: [
|
|
410
|
-
{sha256: 'b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c'},
|
|
411
|
-
{sha256: '73a2c64f9545172c1195efb6616ca5f7afd1df6f245407cafb90de3998a1c97f'}
|
|
412
|
-
],
|
|
413
|
-
report_only: true, # defaults to false (report-only mode)
|
|
414
|
-
report_uri: 'https://report-uri.io/example-hpkp'
|
|
415
|
-
}
|
|
416
|
-
```
|
|
417
|
-
|
|
418
|
-
### Cookies
|
|
419
|
-
|
|
420
|
-
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.
|
|
421
|
-
|
|
422
|
-
__Note__: Regardless of the configuration specified, Secure cookies are only enabled for HTTPS requests.
|
|
423
|
-
|
|
424
|
-
#### Boolean-based configuration
|
|
425
|
-
|
|
426
|
-
Boolean-based configuration is intended to globally enable or disable a specific cookie attribute.
|
|
427
|
-
|
|
428
|
-
```ruby
|
|
429
|
-
config.cookies = {
|
|
430
|
-
secure: true, # mark all cookies as Secure
|
|
431
|
-
httponly: false, # do not mark any cookies as HttpOnly
|
|
432
|
-
}
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
#### Hash-based configuration
|
|
436
|
-
|
|
437
|
-
Hash-based configuration allows for fine-grained control.
|
|
438
|
-
|
|
439
|
-
```ruby
|
|
440
|
-
config.cookies = {
|
|
441
|
-
secure: { except: ['_guest'] }, # mark all but the `_guest` cookie as Secure
|
|
442
|
-
httponly: { only: ['_rails_session'] }, # only mark the `_rails_session` cookie as HttpOnly
|
|
443
|
-
}
|
|
444
|
-
```
|
|
445
|
-
|
|
446
|
-
#### SameSite cookie configuration
|
|
447
|
-
|
|
448
|
-
SameSite cookies permit either `Strict` or `Lax` enforcement mode options.
|
|
449
|
-
|
|
450
|
-
```ruby
|
|
451
|
-
config.cookies = {
|
|
452
|
-
samesite: {
|
|
453
|
-
strict: true # mark all cookies as SameSite=Strict
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
```
|
|
457
|
-
|
|
458
|
-
`Strict` and `Lax` enforcement modes can also be specified using a Hash.
|
|
459
|
-
|
|
460
|
-
```ruby
|
|
461
|
-
config.cookies = {
|
|
462
|
-
samesite: {
|
|
463
|
-
strict: { only: ['_rails_session'] },
|
|
464
|
-
lax: { only: ['_guest'] }
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
```
|
|
468
|
-
|
|
469
|
-
### Using with Sinatra
|
|
470
|
-
|
|
471
|
-
Here's an example using SecureHeaders for Sinatra applications:
|
|
472
|
-
|
|
473
|
-
```ruby
|
|
474
|
-
require 'rubygems'
|
|
475
|
-
require 'sinatra'
|
|
476
|
-
require 'haml'
|
|
477
|
-
require 'secure_headers'
|
|
478
|
-
|
|
479
|
-
use SecureHeaders::Middleware
|
|
480
|
-
|
|
481
|
-
SecureHeaders::Configuration.default do |config|
|
|
482
|
-
...
|
|
483
|
-
end
|
|
484
|
-
|
|
485
|
-
class Donkey < Sinatra::Application
|
|
486
|
-
set :root, APP_ROOT
|
|
487
|
-
|
|
488
|
-
get '/' do
|
|
489
|
-
SecureHeaders.override_x_frame_options(request, SecureHeaders::OPT_OUT)
|
|
490
|
-
haml :index
|
|
491
|
-
end
|
|
492
|
-
end
|
|
493
|
-
```
|
|
494
146
|
|
|
495
147
|
## Similar libraries
|
|
496
148
|
|
data/docs/HPKP.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
## HTTP Public Key Pins
|
|
2
|
+
|
|
3
|
+
Be aware that pinning error reporting is governed by the same rules as everything else. If you have a pinning failure that tries to report back to the same origin, by definition this will not work.
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
config.hpkp = {
|
|
7
|
+
max_age: 60.days.to_i, # max_age is a required parameter
|
|
8
|
+
include_subdomains: true, # whether or not to apply pins to subdomains
|
|
9
|
+
# Per the spec, SHA256 hashes are the only currently supported format.
|
|
10
|
+
pins: [
|
|
11
|
+
{sha256: 'b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c'},
|
|
12
|
+
{sha256: '73a2c64f9545172c1195efb6616ca5f7afd1df6f245407cafb90de3998a1c97f'}
|
|
13
|
+
],
|
|
14
|
+
report_only: true, # defaults to false (report-only mode)
|
|
15
|
+
report_uri: 'https://report-uri.io/example-hpkp'
|
|
16
|
+
}
|
|
17
|
+
```
|
data/docs/cookies.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
## Cookies
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
__Note__: Regardless of the configuration specified, Secure cookies are only enabled for HTTPS requests.
|
|
6
|
+
|
|
7
|
+
#### Boolean-based configuration
|
|
8
|
+
|
|
9
|
+
Boolean-based configuration is intended to globally enable or disable a specific cookie attribute.
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
config.cookies = {
|
|
13
|
+
secure: true, # mark all cookies as Secure
|
|
14
|
+
httponly: false, # do not mark any cookies as HttpOnly
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
#### Hash-based configuration
|
|
19
|
+
|
|
20
|
+
Hash-based configuration allows for fine-grained control.
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
config.cookies = {
|
|
24
|
+
secure: { except: ['_guest'] }, # mark all but the `_guest` cookie as Secure
|
|
25
|
+
httponly: { only: ['_rails_session'] }, # only mark the `_rails_session` cookie as HttpOnly
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
#### SameSite cookie configuration
|
|
30
|
+
|
|
31
|
+
SameSite cookies permit either `Strict` or `Lax` enforcement mode options.
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
config.cookies = {
|
|
35
|
+
samesite: {
|
|
36
|
+
strict: true # mark all cookies as SameSite=Strict
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`Strict` and `Lax` enforcement modes can also be specified using a Hash.
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
config.cookies = {
|
|
45
|
+
samesite: {
|
|
46
|
+
strict: { only: ['_rails_session'] },
|
|
47
|
+
lax: { only: ['_guest'] }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
data/docs/hashes.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
## Hash
|
|
2
|
+
|
|
3
|
+
`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`.
|
|
4
|
+
|
|
5
|
+
You can add hash sources directly to your policy :
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
::SecureHeaders::Configuration.default do |config|
|
|
9
|
+
config.csp = {
|
|
10
|
+
default_src: %w('self')
|
|
11
|
+
|
|
12
|
+
# this is a made up value but browsers will show the expected hash in the console.
|
|
13
|
+
script_src: %w(sha256-123456)
|
|
14
|
+
}
|
|
15
|
+
end
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
You can also use the automated inline script detection/collection/computation of hash source values in your app.
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
rake secure_headers:generate_hashes
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
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.
|
|
25
|
+
|
|
26
|
+
```yaml
|
|
27
|
+
---
|
|
28
|
+
scripts:
|
|
29
|
+
app/views/asdfs/index.html.erb:
|
|
30
|
+
- "'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg='"
|
|
31
|
+
styles:
|
|
32
|
+
app/views/asdfs/index.html.erb:
|
|
33
|
+
- "'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY='"
|
|
34
|
+
- "'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE='"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
##### Helpers
|
|
38
|
+
|
|
39
|
+
**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.
|
|
40
|
+
|
|
41
|
+
```erb
|
|
42
|
+
<%= hashed_style_tag do %>
|
|
43
|
+
body {
|
|
44
|
+
background-color: black;
|
|
45
|
+
}
|
|
46
|
+
<% end %>
|
|
47
|
+
|
|
48
|
+
<%= hashed_style_tag do %>
|
|
49
|
+
body {
|
|
50
|
+
font-size: 30px;
|
|
51
|
+
font-color: green;
|
|
52
|
+
}
|
|
53
|
+
<% end %>
|
|
54
|
+
|
|
55
|
+
<%= hashed_javascript_tag do %>
|
|
56
|
+
console.log(1)
|
|
57
|
+
<% end %>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
Content-Security-Policy: ...
|
|
62
|
+
script-src 'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg=' ... ;
|
|
63
|
+
style-src 'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY=' 'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE=' ...;
|
|
64
|
+
```
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
## Named Appends
|
|
2
|
+
|
|
3
|
+
Named Appends are blocks of code that can be reused and composed during requests. e.g. If a certain partial is rendered conditionally, and the csp needs to be adjusted for that partial, you can create a named append for that situation. The value returned by the block will be passed into `append_content_security_policy_directives`. The current request object is passed as an argument to the block for even more flexibility.
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
def show
|
|
7
|
+
if include_widget?
|
|
8
|
+
@widget = widget.render
|
|
9
|
+
use_content_security_policy_named_append(:widget_partial)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
SecureHeaders::Configuration.named_append(:widget_partial) do |request|
|
|
15
|
+
SecureHeaders.override_x_frame_options(request, "DENY")
|
|
16
|
+
if request.controller_instance.current_user.in_test_bucket?
|
|
17
|
+
{ child_src: %w(beta.thirdpartyhost.com) }
|
|
18
|
+
else
|
|
19
|
+
{ child_src: %w(thirdpartyhost.com) }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
You can use as many named appends as you would like per request, but be careful because order of inclusion matters. Consider the following:
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
SecureHeader::Configuration.default do |config|
|
|
28
|
+
config.csp = { default_src: %w('self')}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
SecureHeaders::Configuration.named_append(:A) do |request|
|
|
32
|
+
{ default_src: %w(myhost.com) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
SecureHeaders::Configuration.named_append(:B) do |request|
|
|
36
|
+
{ script_src: %w('unsafe-eval') }
|
|
37
|
+
end
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The following code will produce different policies due to the way policies are normalized (e.g. providing a previously undefined directive that inherits from `default-src`, removing host source values when `*` is provided. Removing `'none'` when additional values are present, etc.):
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
def index
|
|
44
|
+
use_content_security_policy_named_append(:A)
|
|
45
|
+
use_content_security_policy_named_append(:B)
|
|
46
|
+
# produces default-src 'self' myhost.com; script-src 'self' myhost.com 'unsafe-eval';
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def show
|
|
50
|
+
use_content_security_policy_named_append(:B)
|
|
51
|
+
use_content_security_policy_named_append(:A)
|
|
52
|
+
# produces default-src 'self' myhost.com; script-src 'self' 'unsafe-eval';
|
|
53
|
+
end
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
## Named overrides
|
|
58
|
+
|
|
59
|
+
Named overrides serve two purposes:
|
|
60
|
+
|
|
61
|
+
* To be able to refer to a configuration by simple name.
|
|
62
|
+
* By precomputing the headers for a named configuration, the headers generated once and reused over every request.
|
|
63
|
+
|
|
64
|
+
To use a named override, drop a `SecureHeaders::Configuration.override` block **outside** of method definitions and then declare which named override you'd like to use. You can even override an override.
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
class ApplicationController < ActionController::Base
|
|
68
|
+
SecureHeaders::Configuration.default do |config|
|
|
69
|
+
config.csp = {
|
|
70
|
+
default_src: %w('self'),
|
|
71
|
+
script_src: %w(example.org)
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# override default configuration
|
|
76
|
+
SecureHeaders::Configuration.override(:script_from_otherdomain_com) do |config|
|
|
77
|
+
config.csp[:script_src] << "otherdomain.com"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# overrides the :script_from_otherdomain_com configuration
|
|
81
|
+
SecureHeaders::Configuration.override(:another_config, :script_from_otherdomain_com) do |config|
|
|
82
|
+
config.csp[:script_src] << "evenanotherdomain.com"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
class MyController < ApplicationController
|
|
87
|
+
def index
|
|
88
|
+
# Produces default-src 'self'; script-src example.org otherdomain.com
|
|
89
|
+
use_secure_headers_override(:script_from_otherdomain_com)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def show
|
|
93
|
+
# Produces default-src 'self'; script-src example.org otherdomain.org evenanotherdomain.com
|
|
94
|
+
use_secure_headers_override(:another_config)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
By default, a no-op configuration is provided. No headers will be set when this default override is used.
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
class MyController < ApplicationController
|
|
103
|
+
def index
|
|
104
|
+
SecureHeaders.opt_out_of_all_protection(request)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
```
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
## Per-action configuration
|
|
2
|
+
|
|
3
|
+
You can override the settings for a given action by producing a temporary override. Be aware that because of the dynamic nature of the value, the header values will be computed per request.
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
# Given a config of:
|
|
7
|
+
::SecureHeaders::Configuration.default do |config|
|
|
8
|
+
config.csp = {
|
|
9
|
+
default_src: %w('self'),
|
|
10
|
+
script_src: %w('self')
|
|
11
|
+
}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class MyController < ApplicationController
|
|
15
|
+
def index
|
|
16
|
+
# Append value to the source list, override 'none' values
|
|
17
|
+
# Produces: default-src 'self'; script-src 'self' s3.amazonaws.com; object-src 'self' www.youtube.com
|
|
18
|
+
append_content_security_policy_directives(script_src: %w(s3.amazonaws.com), object_src: %w('self' www.youtube.com))
|
|
19
|
+
|
|
20
|
+
# Overrides the previously set source list, override 'none' values
|
|
21
|
+
# Produces: default-src 'self'; script-src s3.amazonaws.com; object-src 'self'
|
|
22
|
+
override_content_security_policy_directives(script_src: %w(s3.amazonaws.com), object_src: %w('self'))
|
|
23
|
+
|
|
24
|
+
# Global settings default to "sameorigin"
|
|
25
|
+
override_x_frame_options("DENY")
|
|
26
|
+
end
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The following methods are available as controller instance methods. They are also available as class methods, but require you to pass in the `request` object.
|
|
30
|
+
* `append_content_security_policy_directives(hash)`: appends each value to the corresponding CSP app-wide configuration.
|
|
31
|
+
* `override_content_security_policy_directives(hash)`: merges the hash into the app-wide configuration, overwriting any previous config
|
|
32
|
+
* `override_x_frame_options(value)`: sets the `X-Frame-Options header` to `value`
|
|
33
|
+
|
|
34
|
+
## Appending / overriding Content Security Policy
|
|
35
|
+
|
|
36
|
+
When manipulating content security policy, there are a few things to consider. The default header value is `default-src https:` which corresponds to a default configuration of `{ default_src: %w(https:)}`.
|
|
37
|
+
|
|
38
|
+
#### Append to the policy with a directive other than `default_src`
|
|
39
|
+
|
|
40
|
+
The value of `default_src` is joined with the addition if the it is a [fetch directive](https://w3c.github.io/webappsec-csp/#directives-fetch). Note the `https:` is carried over from the `default-src` config. If you do not want this, use `override_content_security_policy_directives` instead. To illustrate:
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
::SecureHeaders::Configuration.default do |config|
|
|
44
|
+
config.csp = {
|
|
45
|
+
default_src: %w('self')
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Code | Result
|
|
51
|
+
------------- | -------------
|
|
52
|
+
`append_content_security_policy_directives(script_src: %w(mycdn.com))` | `default-src 'self'; script-src 'self' mycdn.com`
|
|
53
|
+
`override_content_security_policy_directives(script_src: %w(mycdn.com))` | `default-src 'self'; script-src mycdn.com`
|
|
54
|
+
|
|
55
|
+
#### Nonce
|
|
56
|
+
|
|
57
|
+
You can use a view helper to automatically add nonces to script tags:
|
|
58
|
+
|
|
59
|
+
```erb
|
|
60
|
+
<%= nonced_javascript_tag do %>
|
|
61
|
+
console.log("nonced!");
|
|
62
|
+
<% end %>
|
|
63
|
+
|
|
64
|
+
<%= nonced_style_tag do %>
|
|
65
|
+
body {
|
|
66
|
+
background-color: black;
|
|
67
|
+
}
|
|
68
|
+
<% end %>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
becomes:
|
|
72
|
+
|
|
73
|
+
```html
|
|
74
|
+
<script nonce="/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=">
|
|
75
|
+
console.log("nonced!")
|
|
76
|
+
</script>
|
|
77
|
+
<style nonce="/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=">
|
|
78
|
+
body {
|
|
79
|
+
background-color: black;
|
|
80
|
+
}
|
|
81
|
+
</style>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Content-Security-Policy: ...
|
|
87
|
+
script-src 'nonce-/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=' ...;
|
|
88
|
+
style-src 'nonce-/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=' ...;
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`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.
|
|
92
|
+
|
|
93
|
+
```erb
|
|
94
|
+
<script nonce="<%= content_security_policy_script_nonce %>">
|
|
95
|
+
console.log("whitelisted, will execute")
|
|
96
|
+
</script>
|
|
97
|
+
|
|
98
|
+
<script nonce="lol">
|
|
99
|
+
console.log("won't execute, not whitelisted")
|
|
100
|
+
</script>
|
|
101
|
+
|
|
102
|
+
<script>
|
|
103
|
+
console.log("won't execute, not whitelisted")
|
|
104
|
+
</script>
|
|
105
|
+
```
|
data/docs/sinatra.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
## Sinatra
|
|
2
|
+
|
|
3
|
+
Here's an example using SecureHeaders for Sinatra applications:
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
require 'rubygems'
|
|
7
|
+
require 'sinatra'
|
|
8
|
+
require 'haml'
|
|
9
|
+
require 'secure_headers'
|
|
10
|
+
|
|
11
|
+
use SecureHeaders::Middleware
|
|
12
|
+
|
|
13
|
+
SecureHeaders::Configuration.default do |config|
|
|
14
|
+
...
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class Donkey < Sinatra::Application
|
|
18
|
+
set :root, APP_ROOT
|
|
19
|
+
|
|
20
|
+
get '/' do
|
|
21
|
+
SecureHeaders.override_x_frame_options(request, SecureHeaders::OPT_OUT)
|
|
22
|
+
haml :index
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
```
|
|
@@ -116,7 +116,7 @@ module SecureHeaders
|
|
|
116
116
|
|
|
117
117
|
attr_writer :hsts, :x_frame_options, :x_content_type_options,
|
|
118
118
|
:x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies,
|
|
119
|
-
:referrer_policy
|
|
119
|
+
:referrer_policy, :clear_site_data
|
|
120
120
|
|
|
121
121
|
attr_reader :cached_headers, :csp, :cookies, :csp_report_only, :hpkp, :hpkp_report_host
|
|
122
122
|
|
|
@@ -150,6 +150,7 @@ module SecureHeaders
|
|
|
150
150
|
copy.x_xss_protection = @x_xss_protection
|
|
151
151
|
copy.x_download_options = @x_download_options
|
|
152
152
|
copy.x_permitted_cross_domain_policies = @x_permitted_cross_domain_policies
|
|
153
|
+
copy.clear_site_data = @clear_site_data
|
|
153
154
|
copy.referrer_policy = @referrer_policy
|
|
154
155
|
copy.hpkp = @hpkp
|
|
155
156
|
copy.hpkp_report_host = @hpkp_report_host
|
|
@@ -181,6 +182,7 @@ module SecureHeaders
|
|
|
181
182
|
XXssProtection.validate_config!(@x_xss_protection)
|
|
182
183
|
XDownloadOptions.validate_config!(@x_download_options)
|
|
183
184
|
XPermittedCrossDomainPolicies.validate_config!(@x_permitted_cross_domain_policies)
|
|
185
|
+
ClearSiteData.validate_config!(@clear_site_data)
|
|
184
186
|
PublicKeyPins.validate_config!(@hpkp)
|
|
185
187
|
Cookie.validate_config!(@cookies)
|
|
186
188
|
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module SecureHeaders
|
|
2
|
+
class ClearSiteDataConfigError < StandardError; end
|
|
3
|
+
class ClearSiteData
|
|
4
|
+
HEADER_NAME = "Clear-Site-Data".freeze
|
|
5
|
+
TYPES = "types".freeze
|
|
6
|
+
|
|
7
|
+
# Valid `types`
|
|
8
|
+
CACHE = "cache".freeze
|
|
9
|
+
COOKIES = "cookies".freeze
|
|
10
|
+
STORAGE = "storage".freeze
|
|
11
|
+
EXECTION_CONTEXTS = "executionContexts".freeze
|
|
12
|
+
ALL_TYPES = [CACHE, COOKIES, STORAGE, EXECTION_CONTEXTS]
|
|
13
|
+
|
|
14
|
+
CONFIG_KEY = :clear_site_data
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
# Public: make an Clear-Site-Data header name, value pair
|
|
18
|
+
#
|
|
19
|
+
# Returns nil if not configured, returns header name and value if configured.
|
|
20
|
+
def make_header(config=nil)
|
|
21
|
+
case config
|
|
22
|
+
when nil, OPT_OUT, []
|
|
23
|
+
# noop
|
|
24
|
+
when Array
|
|
25
|
+
[HEADER_NAME, JSON.dump(TYPES => config)]
|
|
26
|
+
when true
|
|
27
|
+
[HEADER_NAME, JSON.dump(TYPES => ALL_TYPES)]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def validate_config!(config)
|
|
32
|
+
case config
|
|
33
|
+
when nil, OPT_OUT, true
|
|
34
|
+
# valid
|
|
35
|
+
when Array
|
|
36
|
+
unless config.all? { |t| t.is_a?(String) }
|
|
37
|
+
raise ClearSiteDataConfigError.new("types must be Strings")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
begin
|
|
41
|
+
JSON.dump(config)
|
|
42
|
+
rescue JSON::GeneratorError, Encoding::UndefinedConversionError
|
|
43
|
+
raise ClearSiteDataConfigError.new("types must serializable by JSON")
|
|
44
|
+
end
|
|
45
|
+
else
|
|
46
|
+
raise ClearSiteDataConfigError.new("config must be an Array of Strings or `true`")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -9,6 +9,7 @@ module SecureHeaders
|
|
|
9
9
|
# constants to be used for version-specific UA sniffing
|
|
10
10
|
VERSION_46 = ::UserAgent::Version.new("46")
|
|
11
11
|
VERSION_10 = ::UserAgent::Version.new("10")
|
|
12
|
+
FALLBACK_VERSION = ::UserAgent::Version.new("0")
|
|
12
13
|
|
|
13
14
|
def initialize(config = nil, user_agent = OTHER)
|
|
14
15
|
@config = if config.is_a?(Hash)
|
|
@@ -213,7 +214,7 @@ module SecureHeaders
|
|
|
213
214
|
# Returns an array of symbols representing the directives.
|
|
214
215
|
def supported_directives
|
|
215
216
|
@supported_directives ||= if VARIATIONS[@parsed_ua.browser]
|
|
216
|
-
if @parsed_ua.browser == "Firefox" && @parsed_ua.version >= VERSION_46
|
|
217
|
+
if @parsed_ua.browser == "Firefox" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_46)
|
|
217
218
|
VARIATIONS["FirefoxTransitional"]
|
|
218
219
|
else
|
|
219
220
|
VARIATIONS[@parsed_ua.browser]
|
|
@@ -14,6 +14,7 @@ module SecureHeaders
|
|
|
14
14
|
STAR = "*".freeze
|
|
15
15
|
UNSAFE_INLINE = "'unsafe-inline'".freeze
|
|
16
16
|
UNSAFE_EVAL = "'unsafe-eval'".freeze
|
|
17
|
+
STRICT_DYNAMIC = "'strict-dynamic'".freeze
|
|
17
18
|
|
|
18
19
|
# leftover deprecated values that will be in common use upon upgrading.
|
|
19
20
|
DEPRECATED_SOURCE_VALUES = [SELF, NONE, UNSAFE_EVAL, UNSAFE_INLINE, "inline", "eval"].map { |value| value.delete("'") }.freeze
|
|
@@ -217,7 +218,7 @@ module SecureHeaders
|
|
|
217
218
|
def nonces_supported?(user_agent)
|
|
218
219
|
user_agent = UserAgent.parse(user_agent) if user_agent.is_a?(String)
|
|
219
220
|
MODERN_BROWSERS.include?(user_agent.browser) ||
|
|
220
|
-
user_agent.browser == "Safari" && user_agent.version >= CSP::VERSION_10
|
|
221
|
+
user_agent.browser == "Safari" && (user_agent.version || CSP::FALLBACK_VERSION) >= CSP::VERSION_10
|
|
221
222
|
end
|
|
222
223
|
|
|
223
224
|
# Public: combine the values from two different configs.
|
data/lib/secure_headers.rb
CHANGED
|
@@ -10,6 +10,7 @@ require "secure_headers/headers/x_content_type_options"
|
|
|
10
10
|
require "secure_headers/headers/x_download_options"
|
|
11
11
|
require "secure_headers/headers/x_permitted_cross_domain_policies"
|
|
12
12
|
require "secure_headers/headers/referrer_policy"
|
|
13
|
+
require "secure_headers/headers/clear_site_data"
|
|
13
14
|
require "secure_headers/middleware"
|
|
14
15
|
require "secure_headers/railtie"
|
|
15
16
|
require "secure_headers/view_helper"
|
|
@@ -50,6 +51,7 @@ module SecureHeaders
|
|
|
50
51
|
CSP = ContentSecurityPolicy
|
|
51
52
|
|
|
52
53
|
ALL_HEADER_CLASSES = [
|
|
54
|
+
ClearSiteData,
|
|
53
55
|
ContentSecurityPolicyConfig,
|
|
54
56
|
ContentSecurityPolicyReportOnlyConfig,
|
|
55
57
|
StrictTransportSecurity,
|
data/secure_headers.gemspec
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
|
2
2
|
Gem::Specification.new do |gem|
|
|
3
3
|
gem.name = "secure_headers"
|
|
4
|
-
gem.version = "3.
|
|
4
|
+
gem.version = "3.6.0"
|
|
5
5
|
gem.authors = ["Neil Matatall"]
|
|
6
6
|
gem.email = ["neil.matatall@gmail.com"]
|
|
7
7
|
gem.description = 'Security related headers all in one gem.'
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
module SecureHeaders
|
|
4
|
+
describe ClearSiteData do
|
|
5
|
+
describe "make_header" do
|
|
6
|
+
it "returns nil with nil config" do
|
|
7
|
+
expect(described_class.make_header).to be_nil
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
it "returns nil with empty config" do
|
|
11
|
+
expect(described_class.make_header([])).to be_nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it "returns nil with opt-out config" do
|
|
15
|
+
expect(described_class.make_header(OPT_OUT)).to be_nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "returns all types with `true` config" do
|
|
19
|
+
name, value = described_class.make_header(true)
|
|
20
|
+
|
|
21
|
+
expect(name).to eq(ClearSiteData::HEADER_NAME)
|
|
22
|
+
expect(value).to eq(normalize_json(<<-HERE))
|
|
23
|
+
{
|
|
24
|
+
"types": [
|
|
25
|
+
"cache",
|
|
26
|
+
"cookies",
|
|
27
|
+
"storage",
|
|
28
|
+
"executionContexts"
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
HERE
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "returns specified types" do
|
|
35
|
+
name, value = described_class.make_header(["foo", "bar"])
|
|
36
|
+
|
|
37
|
+
expect(name).to eq(ClearSiteData::HEADER_NAME)
|
|
38
|
+
expect(value).to eq(normalize_json(<<-HERE))
|
|
39
|
+
{
|
|
40
|
+
"types": [
|
|
41
|
+
"foo",
|
|
42
|
+
"bar"
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
HERE
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
describe "validate_config!" do
|
|
50
|
+
it "succeeds for `true` config" do
|
|
51
|
+
expect do
|
|
52
|
+
described_class.validate_config!(true)
|
|
53
|
+
end.not_to raise_error
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "succeeds for `nil` config" do
|
|
57
|
+
expect do
|
|
58
|
+
described_class.validate_config!(nil)
|
|
59
|
+
end.not_to raise_error
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "succeeds for opt-out config" do
|
|
63
|
+
expect do
|
|
64
|
+
described_class.validate_config!(OPT_OUT)
|
|
65
|
+
end.not_to raise_error
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "succeeds for empty config" do
|
|
69
|
+
expect do
|
|
70
|
+
described_class.validate_config!([])
|
|
71
|
+
end.not_to raise_error
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it "succeeds for Array of Strings config" do
|
|
75
|
+
expect do
|
|
76
|
+
described_class.validate_config!(["foo"])
|
|
77
|
+
end.not_to raise_error
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "fails for Array of non-String config" do
|
|
81
|
+
expect do
|
|
82
|
+
described_class.validate_config!([1])
|
|
83
|
+
end.to raise_error(ClearSiteDataConfigError)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "fails for non-serializable config" do
|
|
87
|
+
expect do
|
|
88
|
+
described_class.validate_config!(["hi \255"])
|
|
89
|
+
end.to raise_error(ClearSiteDataConfigError)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "fails for other types of config" do
|
|
93
|
+
expect do
|
|
94
|
+
described_class.validate_config!(:cookies)
|
|
95
|
+
end.to raise_error(ClearSiteDataConfigError)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def normalize_json(json)
|
|
100
|
+
JSON.dump(JSON.parse(json))
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -107,6 +107,11 @@ module SecureHeaders
|
|
|
107
107
|
expect(firefox_transitional).not_to match(/frame-src/)
|
|
108
108
|
end
|
|
109
109
|
|
|
110
|
+
it "supports strict-dynamic" do
|
|
111
|
+
csp = ContentSecurityPolicy.new({default_src: %w('self'), script_src: [ContentSecurityPolicy::STRICT_DYNAMIC], script_nonce: 123456}, USER_AGENTS[:chrome])
|
|
112
|
+
expect(csp.value).to eq("default-src 'self'; script-src 'strict-dynamic' 'nonce-123456'")
|
|
113
|
+
end
|
|
114
|
+
|
|
110
115
|
context "browser sniffing" do
|
|
111
116
|
let (:complex_opts) do
|
|
112
117
|
(ContentSecurityPolicy::ALL_DIRECTIVES - [:frame_src]).each_with_object({}) do |directive, hash|
|
|
@@ -149,6 +154,13 @@ module SecureHeaders
|
|
|
149
154
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari6])
|
|
150
155
|
expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox sandbox.com; script-src script-src.com 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
|
|
151
156
|
end
|
|
157
|
+
|
|
158
|
+
it "falls back to standard Firefox defaults when the useragent version is not present" do
|
|
159
|
+
ua = USER_AGENTS[:firefox].dup
|
|
160
|
+
allow(ua).to receive(:version).and_return(nil)
|
|
161
|
+
policy = ContentSecurityPolicy.new(complex_opts, ua)
|
|
162
|
+
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
|
|
163
|
+
end
|
|
152
164
|
end
|
|
153
165
|
end
|
|
154
166
|
end
|
|
@@ -18,6 +18,24 @@ module SecureHeaders
|
|
|
18
18
|
end.not_to raise_error
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
it "accepts 'same-origin'" do
|
|
22
|
+
expect do
|
|
23
|
+
ReferrerPolicy.validate_config!("same-origin")
|
|
24
|
+
end.not_to raise_error
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "accepts 'strict-origin'" do
|
|
28
|
+
expect do
|
|
29
|
+
ReferrerPolicy.validate_config!("strict-origin")
|
|
30
|
+
end.not_to raise_error
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "accepts 'strict-origin-when-cross-origin'" do
|
|
34
|
+
expect do
|
|
35
|
+
ReferrerPolicy.validate_config!("strict-origin-when-cross-origin")
|
|
36
|
+
end.not_to raise_error
|
|
37
|
+
end
|
|
38
|
+
|
|
21
39
|
it "accepts 'origin'" do
|
|
22
40
|
expect do
|
|
23
41
|
ReferrerPolicy.validate_config!("origin")
|
|
@@ -530,6 +530,14 @@ module SecureHeaders
|
|
|
530
530
|
end.to raise_error(XContentTypeOptionsConfigError)
|
|
531
531
|
end
|
|
532
532
|
|
|
533
|
+
it "validates your clear site data config upon configuration" do
|
|
534
|
+
expect do
|
|
535
|
+
Configuration.default do |config|
|
|
536
|
+
config.clear_site_data = 1
|
|
537
|
+
end
|
|
538
|
+
end.to raise_error(ClearSiteDataConfigError)
|
|
539
|
+
end
|
|
540
|
+
|
|
533
541
|
it "validates your x_xss config upon configuration" do
|
|
534
542
|
expect do
|
|
535
543
|
Configuration.default do |config|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: secure_headers
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Neil Matatall
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2017-01-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rake
|
|
@@ -58,9 +58,16 @@ files:
|
|
|
58
58
|
- LICENSE
|
|
59
59
|
- README.md
|
|
60
60
|
- Rakefile
|
|
61
|
+
- docs/HPKP.md
|
|
62
|
+
- docs/cookies.md
|
|
63
|
+
- docs/hashes.md
|
|
64
|
+
- docs/named_overrides_and_appends.md
|
|
65
|
+
- docs/per_action_configuration.md
|
|
66
|
+
- docs/sinatra.md
|
|
61
67
|
- lib/secure_headers.rb
|
|
62
68
|
- lib/secure_headers/configuration.rb
|
|
63
69
|
- lib/secure_headers/hash_helper.rb
|
|
70
|
+
- lib/secure_headers/headers/clear_site_data.rb
|
|
64
71
|
- lib/secure_headers/headers/content_security_policy.rb
|
|
65
72
|
- lib/secure_headers/headers/content_security_policy_config.rb
|
|
66
73
|
- lib/secure_headers/headers/cookie.rb
|
|
@@ -80,6 +87,7 @@ files:
|
|
|
80
87
|
- lib/tasks/tasks.rake
|
|
81
88
|
- secure_headers.gemspec
|
|
82
89
|
- spec/lib/secure_headers/configuration_spec.rb
|
|
90
|
+
- spec/lib/secure_headers/headers/clear_site_data_spec.rb
|
|
83
91
|
- spec/lib/secure_headers/headers/content_security_policy_spec.rb
|
|
84
92
|
- spec/lib/secure_headers/headers/cookie_spec.rb
|
|
85
93
|
- spec/lib/secure_headers/headers/policy_management_spec.rb
|
|
@@ -123,6 +131,7 @@ summary: Add easily configured security headers to responses including content-s
|
|
|
123
131
|
x-frame-options, strict-transport-security, etc.
|
|
124
132
|
test_files:
|
|
125
133
|
- spec/lib/secure_headers/configuration_spec.rb
|
|
134
|
+
- spec/lib/secure_headers/headers/clear_site_data_spec.rb
|
|
126
135
|
- spec/lib/secure_headers/headers/content_security_policy_spec.rb
|
|
127
136
|
- spec/lib/secure_headers/headers/cookie_spec.rb
|
|
128
137
|
- spec/lib/secure_headers/headers/policy_management_spec.rb
|