secure_headers 3.2.0 → 3.4.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/.github/ISSUE_TEMPLATE.md +41 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +20 -0
- data/CHANGELOG.md +31 -0
- data/Gemfile +2 -1
- data/README.md +21 -22
- data/lib/secure_headers/configuration.rb +30 -3
- data/lib/secure_headers/headers/content_security_policy.rb +34 -4
- data/lib/secure_headers/headers/policy_management.rb +16 -0
- data/lib/secure_headers/headers/referrer_policy.rb +33 -0
- data/lib/secure_headers/middleware.rb +6 -0
- data/lib/secure_headers/railtie.rb +1 -1
- data/lib/secure_headers/view_helper.rb +3 -5
- data/lib/secure_headers.rb +2 -0
- data/lib/tasks/tasks.rake +5 -4
- data/secure_headers.gemspec +1 -1
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +50 -9
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +2 -2
- data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +54 -0
- data/spec/lib/secure_headers/middleware_spec.rb +18 -0
- data/spec/lib/secure_headers_spec.rb +26 -2
- data/spec/spec_helper.rb +4 -1
- data/upgrading-to-3-0.md +13 -10
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 88233251c7a26e1269f77957d56823619e3d9d11
|
|
4
|
+
data.tar.gz: 8e9c0e69fcca0298ee52223d587134f5a8df062f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f421d0d58eb9b5a7b22fd13932644c2c3868e0294886cbf5250127e8807fa20dec95717219e0a82ce59f49f376b32e17b97b77c7e011d2e2d1633162c1297bbc
|
|
7
|
+
data.tar.gz: ae63cbdd588bf6c31e531bfec5480e022a5df96c8ee2cc4c824d1e869f64df2a44073d795cc6d5e7ffeca6d6369f0d2e33f202ce8ceda04d274849e95789ff43
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Feature Requests
|
|
2
|
+
|
|
3
|
+
## Adding a new header
|
|
4
|
+
|
|
5
|
+
Generally, adding a new header is always OK.
|
|
6
|
+
|
|
7
|
+
* Is the header supported by any user agent? If so, which?
|
|
8
|
+
* What does it do?
|
|
9
|
+
* What are the valid values for the header?
|
|
10
|
+
* Where does the specification live?
|
|
11
|
+
|
|
12
|
+
## Adding a new CSP directive
|
|
13
|
+
|
|
14
|
+
* Is the directive supported by any user agent? If so, which?
|
|
15
|
+
* What does it do?
|
|
16
|
+
* What are the valid values for the directive?
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# Bugs
|
|
21
|
+
|
|
22
|
+
Console errors and deprecation warnings are considered bugs that should be addressed with more precise UA sniffing. Bugs caused by incorrect or invalid UA sniffing are also bugs.
|
|
23
|
+
|
|
24
|
+
### Expected outcome
|
|
25
|
+
|
|
26
|
+
Describe what you expected to happen
|
|
27
|
+
|
|
28
|
+
1. I configure CSP to do X
|
|
29
|
+
1. When I inspect the response headers, the CSP should have included X
|
|
30
|
+
|
|
31
|
+
### Actual outcome
|
|
32
|
+
|
|
33
|
+
1. The generated policy did not include X
|
|
34
|
+
|
|
35
|
+
### Config
|
|
36
|
+
|
|
37
|
+
Please provide the configuration (`SecureHeaders::Configuration.default`) you are using including any overrides (`SecureHeaders::Configuration.override`).
|
|
38
|
+
|
|
39
|
+
### Generated headers
|
|
40
|
+
|
|
41
|
+
Provide a sample response containing the headers
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
## All PRs:
|
|
2
|
+
|
|
3
|
+
* [ ] Has tests
|
|
4
|
+
* [ ] Documentation updated
|
|
5
|
+
|
|
6
|
+
## Adding a new header
|
|
7
|
+
|
|
8
|
+
Generally, adding a new header is always OK.
|
|
9
|
+
|
|
10
|
+
* Is the header supported by any user agent? If so, which?
|
|
11
|
+
* What does it do?
|
|
12
|
+
* What are the valid values for the header?
|
|
13
|
+
* Where does the specification live?
|
|
14
|
+
|
|
15
|
+
## Adding a new CSP directive
|
|
16
|
+
|
|
17
|
+
* Is the directive supported by any user agent? If so, which?
|
|
18
|
+
* What does it do?
|
|
19
|
+
* What are the valid values for the directive?
|
|
20
|
+
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
1
|
+
## 3.4.0 the frame-src/child-src transition for Firefox.
|
|
2
|
+
|
|
3
|
+
Handle the `child-src`/`frame-src` transition semi-intelligently across versions. I think the code best descibes the behavior here:
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
if supported_directives.include?(:child_src)
|
|
7
|
+
@config[:child_src] = @config[:child_src] || @config[:frame_src]
|
|
8
|
+
else
|
|
9
|
+
@config[:frame_src] = @config[:frame_src] || @config[:child_src]
|
|
10
|
+
end
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Also, @koenpunt noticed that we were [loading view helpers](https://github.com/twitter/secureheaders/pull/272) in a way that Rails 5 did not like.
|
|
14
|
+
|
|
15
|
+
## 3.3.2 minor fix to silence warnings when using rake
|
|
16
|
+
|
|
17
|
+
[@dankohn](https://github.com/twitter/secureheaders/issues/257) was seeing "already initialized" errors in his output. This change conditionally defines the constants.
|
|
18
|
+
|
|
19
|
+
## 3.3.1 bugfix for boolean CSP directives
|
|
20
|
+
|
|
21
|
+
[@stefansundin](https://github.com/twitter/secureheaders/pull/253) noticed that supplying `false` to "boolean" CSP directives (e.g. `upgrade-insecure-requests` and `block-all-mixed-content`) would still include the value.
|
|
22
|
+
|
|
23
|
+
## 3.3.0 referrer-policy support
|
|
24
|
+
|
|
25
|
+
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).
|
|
26
|
+
|
|
27
|
+
Additionally, two minor enhancements were added this version:
|
|
28
|
+
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.
|
|
29
|
+
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.
|
|
30
|
+
|
|
1
31
|
## 3.2.0 Cookie settings and CSP hash sources
|
|
2
32
|
|
|
3
33
|
### Cookies
|
|
@@ -114,6 +144,7 @@ console.log(1)
|
|
|
114
144
|
Content-Security-Policy: ...
|
|
115
145
|
script-src 'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg=' ... ;
|
|
116
146
|
style-src 'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY=' 'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE=' ...;
|
|
147
|
+
```
|
|
117
148
|
|
|
118
149
|
## 3.1.2 Bug fix for regression
|
|
119
150
|
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -8,11 +8,12 @@
|
|
|
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
10
|
- 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
|
-
- X-Frame-Options (XFO) - Prevents your content from being framed and potentially clickjacked. [X-Frame-Options
|
|
11
|
+
- X-Frame-Options (XFO) - Prevents your content from being framed and potentially clickjacked. [X-Frame-Options Specification](https://tools.ietf.org/html/rfc7034)
|
|
12
12
|
- X-XSS-Protection - [Cross site scripting heuristic filter for IE/Chrome](https://msdn.microsoft.com/en-us/library/dd565647\(v=vs.85\).aspx)
|
|
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
19
|
It can also mark all http cookies with the Secure, HttpOnly and SameSite attributes (when configured to do so).
|
|
@@ -44,27 +45,27 @@ SecureHeaders::Configuration.default do |config|
|
|
|
44
45
|
config.x_xss_protection = "1; mode=block"
|
|
45
46
|
config.x_download_options = "noopen"
|
|
46
47
|
config.x_permitted_cross_domain_policies = "none"
|
|
48
|
+
config.referrer_policy = "origin-when-cross-origin"
|
|
47
49
|
config.csp = {
|
|
48
50
|
# "meta" values. these will shaped the header, but the values are not included in the header.
|
|
49
|
-
report_only:
|
|
51
|
+
report_only: true, # default: false
|
|
50
52
|
preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content.
|
|
51
53
|
|
|
52
54
|
# directive values: these values will directly translate into source directives
|
|
53
55
|
default_src: %w(https: 'self'),
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
base_uri: %w('self'),
|
|
57
|
+
block_all_mixed_content: true, # see http://www.w3.org/TR/mixed-content/
|
|
58
|
+
child_src: %w('self'), # if child-src isn't supported, the value for frame-src will be set.
|
|
59
|
+
connect_src: %w(wss:),
|
|
56
60
|
font_src: %w('self' data:),
|
|
61
|
+
form_action: %w('self' github.com),
|
|
62
|
+
frame_ancestors: %w('none'),
|
|
57
63
|
img_src: %w(mycdn.com data:),
|
|
58
64
|
media_src: %w(utoob.com),
|
|
59
65
|
object_src: %w('self'),
|
|
66
|
+
plugin_types: %w(application/x-shockwave-flash),
|
|
60
67
|
script_src: %w('self'),
|
|
61
68
|
style_src: %w('unsafe-inline'),
|
|
62
|
-
base_uri: %w('self'),
|
|
63
|
-
child_src: %w('self'),
|
|
64
|
-
form_action: %w('self' github.com),
|
|
65
|
-
frame_ancestors: %w('none'),
|
|
66
|
-
plugin_types: %w(application/x-shockwave-flash),
|
|
67
|
-
block_all_mixed_content: true, # see [http://www.w3.org/TR/mixed-content/](http://www.w3.org/TR/mixed-content/)
|
|
68
69
|
upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/
|
|
69
70
|
report_uri: %w(https://report-uri.io/example-csp)
|
|
70
71
|
}
|
|
@@ -83,7 +84,7 @@ end
|
|
|
83
84
|
|
|
84
85
|
### rails 2
|
|
85
86
|
|
|
86
|
-
For rails 3+ applications, `secure_headers` has a `railtie` that should automatically include the middleware. For rails 2 applications, an explicit statement is required to use the middleware component.
|
|
87
|
+
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.
|
|
87
88
|
|
|
88
89
|
```ruby
|
|
89
90
|
use SecureHeaders::Middleware
|
|
@@ -135,7 +136,7 @@ class MyController < ApplicationController
|
|
|
135
136
|
end
|
|
136
137
|
```
|
|
137
138
|
|
|
138
|
-
By default, a
|
|
139
|
+
By default, a no-op configuration is provided. No headers will be set when this default override is used.
|
|
139
140
|
|
|
140
141
|
```ruby
|
|
141
142
|
class MyController < ApplicationController
|
|
@@ -161,12 +162,12 @@ You can override the settings for a given action by producing a temporary overri
|
|
|
161
162
|
class MyController < ApplicationController
|
|
162
163
|
def index
|
|
163
164
|
# Append value to the source list, override 'none' values
|
|
164
|
-
# Produces: default-src 'self'; script-src 'self' s3.
|
|
165
|
-
append_content_security_policy_directives(script_src: %w(s3.
|
|
165
|
+
# Produces: default-src 'self'; script-src 'self' s3.amazonaws.com; object-src 'self' www.youtube.com
|
|
166
|
+
append_content_security_policy_directives(script_src: %w(s3.amazonaws.com), object_src: %w('self' www.youtube.com))
|
|
166
167
|
|
|
167
168
|
# Overrides the previously set source list, override 'none' values
|
|
168
|
-
# Produces: default-src 'self'; script-src s3.
|
|
169
|
-
override_content_security_policy_directives(script_src: %w(s3.
|
|
169
|
+
# Produces: default-src 'self'; script-src s3.amazonaws.com; object-src 'self'
|
|
170
|
+
override_content_security_policy_directives(script_src: %w(s3.amazonaws.com), object_src: %w('self'))
|
|
170
171
|
|
|
171
172
|
# Global settings default to "sameorigin"
|
|
172
173
|
override_x_frame_options("DENY")
|
|
@@ -205,7 +206,7 @@ You can use a view helper to automatically add nonces to script tags:
|
|
|
205
206
|
|
|
206
207
|
```erb
|
|
207
208
|
<%= nonced_javascript_tag do %>
|
|
208
|
-
console.log("
|
|
209
|
+
console.log("nonced!");
|
|
209
210
|
<% end %>
|
|
210
211
|
|
|
211
212
|
<%= nonced_style_tag do %>
|
|
@@ -322,17 +323,15 @@ Be aware that pinning error reporting is governed by the same rules as everythin
|
|
|
322
323
|
|
|
323
324
|
```ruby
|
|
324
325
|
config.hpkp = {
|
|
325
|
-
max_age: 60.days.to_i,
|
|
326
|
+
max_age: 60.days.to_i, # max_age is a required parameter
|
|
326
327
|
include_subdomains: true, # whether or not to apply pins to subdomains
|
|
327
328
|
# Per the spec, SHA256 hashes are the only currently supported format.
|
|
328
329
|
pins: [
|
|
329
330
|
{sha256: 'b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c'},
|
|
330
331
|
{sha256: '73a2c64f9545172c1195efb6616ca5f7afd1df6f245407cafb90de3998a1c97f'}
|
|
331
332
|
],
|
|
332
|
-
report_only: true,
|
|
333
|
-
report_uri: 'https://report-uri.io/example-hpkp'
|
|
334
|
-
app_name: 'example',
|
|
335
|
-
tag_report_uri: true
|
|
333
|
+
report_only: true, # defaults to false (report-only mode)
|
|
334
|
+
report_uri: 'https://report-uri.io/example-hpkp'
|
|
336
335
|
}
|
|
337
336
|
```
|
|
338
337
|
|
|
@@ -61,6 +61,7 @@ module SecureHeaders
|
|
|
61
61
|
config.validate_config!
|
|
62
62
|
@configurations ||= {}
|
|
63
63
|
config.send(:cache_headers!)
|
|
64
|
+
config.send(:cache_hpkp_report_host)
|
|
64
65
|
config.freeze
|
|
65
66
|
@configurations[name] = config
|
|
66
67
|
end
|
|
@@ -102,11 +103,13 @@ module SecureHeaders
|
|
|
102
103
|
end
|
|
103
104
|
end
|
|
104
105
|
|
|
106
|
+
attr_accessor :dynamic_csp
|
|
107
|
+
|
|
105
108
|
attr_writer :hsts, :x_frame_options, :x_content_type_options,
|
|
106
109
|
:x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies,
|
|
107
|
-
:
|
|
110
|
+
:referrer_policy
|
|
108
111
|
|
|
109
|
-
attr_reader :cached_headers, :csp, :
|
|
112
|
+
attr_reader :cached_headers, :csp, :cookies, :hpkp, :hpkp_report_host
|
|
110
113
|
|
|
111
114
|
HASH_CONFIG_FILE = ENV["secure_headers_generated_hashes_file"] || "config/secure_headers_generated_hashes.yml"
|
|
112
115
|
if File.exists?(HASH_CONFIG_FILE)
|
|
@@ -117,6 +120,7 @@ module SecureHeaders
|
|
|
117
120
|
|
|
118
121
|
def initialize(&block)
|
|
119
122
|
self.hpkp = OPT_OUT
|
|
123
|
+
self.referrer_policy = OPT_OUT
|
|
120
124
|
self.csp = self.class.send(:deep_copy, CSP::DEFAULT_CONFIG)
|
|
121
125
|
instance_eval &block if block_given?
|
|
122
126
|
end
|
|
@@ -136,7 +140,9 @@ module SecureHeaders
|
|
|
136
140
|
copy.x_xss_protection = @x_xss_protection
|
|
137
141
|
copy.x_download_options = @x_download_options
|
|
138
142
|
copy.x_permitted_cross_domain_policies = @x_permitted_cross_domain_policies
|
|
143
|
+
copy.referrer_policy = @referrer_policy
|
|
139
144
|
copy.hpkp = @hpkp
|
|
145
|
+
copy.hpkp_report_host = @hpkp_report_host
|
|
140
146
|
copy
|
|
141
147
|
end
|
|
142
148
|
|
|
@@ -175,6 +181,7 @@ module SecureHeaders
|
|
|
175
181
|
def validate_config!
|
|
176
182
|
StrictTransportSecurity.validate_config!(@hsts)
|
|
177
183
|
ContentSecurityPolicy.validate_config!(@csp)
|
|
184
|
+
ReferrerPolicy.validate_config!(@referrer_policy)
|
|
178
185
|
XFrameOptions.validate_config!(@x_frame_options)
|
|
179
186
|
XContentTypeOptions.validate_config!(@x_content_type_options)
|
|
180
187
|
XXssProtection.validate_config!(@x_xss_protection)
|
|
@@ -196,15 +203,35 @@ module SecureHeaders
|
|
|
196
203
|
raise IllegalPolicyModificationError, "You are attempting to modify CSP settings directly. Use dynamic_csp= instead."
|
|
197
204
|
end
|
|
198
205
|
|
|
199
|
-
@csp = new_csp
|
|
206
|
+
@csp = self.class.send(:deep_copy_if_hash, new_csp)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def cookies=(cookies)
|
|
210
|
+
@cookies = cookies
|
|
200
211
|
end
|
|
201
212
|
|
|
202
213
|
def cached_headers=(headers)
|
|
203
214
|
@cached_headers = headers
|
|
204
215
|
end
|
|
205
216
|
|
|
217
|
+
def hpkp=(hpkp)
|
|
218
|
+
@hpkp = self.class.send(:deep_copy_if_hash, hpkp)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def hpkp_report_host=(hpkp_report_host)
|
|
222
|
+
@hpkp_report_host = hpkp_report_host
|
|
223
|
+
end
|
|
224
|
+
|
|
206
225
|
private
|
|
207
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
|
+
|
|
208
235
|
# Public: Precompute the header names and values for this configuraiton.
|
|
209
236
|
# Ensures that headers generated at configure time, not on demand.
|
|
210
237
|
#
|
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
require_relative 'policy_management'
|
|
2
|
+
require 'useragent'
|
|
2
3
|
|
|
3
4
|
module SecureHeaders
|
|
4
5
|
class ContentSecurityPolicyConfigError < StandardError; end
|
|
5
6
|
class ContentSecurityPolicy
|
|
6
7
|
include PolicyManagement
|
|
7
8
|
|
|
9
|
+
# constants to be used for version-specific UA sniffing
|
|
10
|
+
VERSION_46 = ::UserAgent::Version.new("46")
|
|
11
|
+
|
|
8
12
|
def initialize(config = nil, user_agent = OTHER)
|
|
9
|
-
config = Configuration.deep_copy
|
|
10
|
-
@config = config
|
|
13
|
+
@config = Configuration.send(:deep_copy, config || DEFAULT_CONFIG)
|
|
11
14
|
@parsed_ua = if user_agent.is_a?(UserAgent::Browsers::Base)
|
|
12
15
|
user_agent
|
|
13
16
|
else
|
|
14
17
|
UserAgent.parse(user_agent)
|
|
15
18
|
end
|
|
19
|
+
normalize_child_frame_src
|
|
16
20
|
@report_only = @config[:report_only]
|
|
17
21
|
@preserve_schemes = @config[:preserve_schemes]
|
|
18
22
|
@script_nonce = @config[:script_nonce]
|
|
@@ -42,6 +46,22 @@ module SecureHeaders
|
|
|
42
46
|
|
|
43
47
|
private
|
|
44
48
|
|
|
49
|
+
# frame-src is deprecated, child-src is being implemented. They are
|
|
50
|
+
# very similar and in most cases, the same value can be used for both.
|
|
51
|
+
def normalize_child_frame_src
|
|
52
|
+
if @config[:frame_src] && @config[:child_src] && @config[:frame_src] != @config[:child_src]
|
|
53
|
+
Kernel.warn("#{Kernel.caller.first}: [DEPRECATION] both :child_src and :frame_src supplied and do not match. This can lead to inconsistent behavior across browsers.")
|
|
54
|
+
elsif @config[:frame_src]
|
|
55
|
+
Kernel.warn("#{Kernel.caller.first}: [DEPRECATION] :frame_src is deprecated, use :child_src instead. Provided: #{@config[:frame_src]}.")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
if supported_directives.include?(:child_src)
|
|
59
|
+
@config[:child_src] = @config[:child_src] || @config[:frame_src]
|
|
60
|
+
else
|
|
61
|
+
@config[:frame_src] = @config[:frame_src] || @config[:child_src]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
45
65
|
# Private: converts the config object into a string representing a policy.
|
|
46
66
|
# Places default-src at the first directive and report-uri as the last. All
|
|
47
67
|
# others are presented in alphabetical order.
|
|
@@ -53,7 +73,7 @@ module SecureHeaders
|
|
|
53
73
|
directives.map do |directive_name|
|
|
54
74
|
case DIRECTIVE_VALUE_TYPES[directive_name]
|
|
55
75
|
when :boolean
|
|
56
|
-
symbol_to_hyphen_case(directive_name)
|
|
76
|
+
symbol_to_hyphen_case(directive_name) if @config[directive_name]
|
|
57
77
|
when :string
|
|
58
78
|
[symbol_to_hyphen_case(directive_name), @config[directive_name]].join(" ")
|
|
59
79
|
else
|
|
@@ -162,9 +182,19 @@ module SecureHeaders
|
|
|
162
182
|
|
|
163
183
|
# Private: determine which directives are supported for the given user agent.
|
|
164
184
|
#
|
|
185
|
+
# Add UA-sniffing special casing here.
|
|
186
|
+
#
|
|
165
187
|
# Returns an array of symbols representing the directives.
|
|
166
188
|
def supported_directives
|
|
167
|
-
@supported_directives ||= VARIATIONS[@parsed_ua.browser]
|
|
189
|
+
@supported_directives ||= if VARIATIONS[@parsed_ua.browser]
|
|
190
|
+
if @parsed_ua.browser == "Firefox" && @parsed_ua.version >= VERSION_46
|
|
191
|
+
VARIATIONS["FirefoxTransitional"]
|
|
192
|
+
else
|
|
193
|
+
VARIATIONS[@parsed_ua.browser]
|
|
194
|
+
end
|
|
195
|
+
else
|
|
196
|
+
VARIATIONS[OTHER]
|
|
197
|
+
end
|
|
168
198
|
end
|
|
169
199
|
|
|
170
200
|
def nonces_supported?
|
|
@@ -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 = [
|
|
@@ -99,10 +100,23 @@ module SecureHeaders
|
|
|
99
100
|
PLUGIN_TYPES
|
|
100
101
|
].freeze
|
|
101
102
|
|
|
103
|
+
FIREFOX_46_DEPRECATED_DIRECTIVES = [
|
|
104
|
+
FRAME_SRC
|
|
105
|
+
].freeze
|
|
106
|
+
|
|
107
|
+
FIREFOX_46_UNSUPPORTED_DIRECTIVES = [
|
|
108
|
+
BLOCK_ALL_MIXED_CONTENT,
|
|
109
|
+
PLUGIN_TYPES
|
|
110
|
+
].freeze
|
|
111
|
+
|
|
102
112
|
FIREFOX_DIRECTIVES = (
|
|
103
113
|
DIRECTIVES_2_0 + DIRECTIVES_DRAFT - FIREFOX_UNSUPPORTED_DIRECTIVES
|
|
104
114
|
).freeze
|
|
105
115
|
|
|
116
|
+
FIREFOX_46_DIRECTIVES = (
|
|
117
|
+
DIRECTIVES_2_0 + DIRECTIVES_DRAFT - FIREFOX_46_UNSUPPORTED_DIRECTIVES - FIREFOX_46_DEPRECATED_DIRECTIVES
|
|
118
|
+
).freeze
|
|
119
|
+
|
|
106
120
|
CHROME_DIRECTIVES = (
|
|
107
121
|
DIRECTIVES_2_0 + DIRECTIVES_DRAFT
|
|
108
122
|
).freeze
|
|
@@ -117,7 +131,9 @@ module SecureHeaders
|
|
|
117
131
|
"Chrome" => CHROME_DIRECTIVES,
|
|
118
132
|
"Opera" => CHROME_DIRECTIVES,
|
|
119
133
|
"Firefox" => FIREFOX_DIRECTIVES,
|
|
134
|
+
"FirefoxTransitional" => FIREFOX_46_DIRECTIVES,
|
|
120
135
|
"Safari" => SAFARI_DIRECTIVES,
|
|
136
|
+
"Edge" => EDGE_DIRECTIVES,
|
|
121
137
|
"Other" => CHROME_DIRECTIVES
|
|
122
138
|
}.freeze
|
|
123
139
|
|
|
@@ -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
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
module SecureHeaders
|
|
2
2
|
class Middleware
|
|
3
|
+
HPKP_SAME_HOST_WARNING = "[WARNING] HPKP report host should not be the same as the request host. See https://github.com/twitter/secureheaders/issues/166"
|
|
4
|
+
|
|
3
5
|
def initialize(app)
|
|
4
6
|
@app = app
|
|
5
7
|
end
|
|
@@ -10,6 +12,10 @@ module SecureHeaders
|
|
|
10
12
|
status, headers, response = @app.call(env)
|
|
11
13
|
|
|
12
14
|
config = SecureHeaders.config_for(req)
|
|
15
|
+
if config.hpkp_report_host == req.host
|
|
16
|
+
Kernel.warn(HPKP_SAME_HOST_WARNING)
|
|
17
|
+
end
|
|
18
|
+
|
|
13
19
|
flag_cookies!(headers, override_secure(env, config.cookies)) if config.cookies
|
|
14
20
|
headers.merge!(SecureHeaders.header_hash_for(req))
|
|
15
21
|
[status, headers, response]
|
|
@@ -7,7 +7,7 @@ if defined?(Rails::Railtie)
|
|
|
7
7
|
'X-Permitted-Cross-Domain-Policies', 'X-Download-Options',
|
|
8
8
|
'X-Content-Type-Options', 'Strict-Transport-Security',
|
|
9
9
|
'Content-Security-Policy', 'Content-Security-Policy-Report-Only',
|
|
10
|
-
'Public-Key-Pins', 'Public-Key-Pins-Report-Only']
|
|
10
|
+
'Public-Key-Pins', 'Public-Key-Pins-Report-Only', 'Referrer-Policy']
|
|
11
11
|
|
|
12
12
|
initializer "secure_headers.middleware" do
|
|
13
13
|
Rails.application.config.middleware.insert_before 0, SecureHeaders::Middleware
|
|
@@ -108,8 +108,6 @@ module SecureHeaders
|
|
|
108
108
|
end
|
|
109
109
|
end
|
|
110
110
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
end
|
|
115
|
-
end
|
|
111
|
+
ActiveSupport.on_load :action_view do
|
|
112
|
+
include SecureHeaders::ViewHelpers
|
|
113
|
+
end if defined?(ActiveSupport)
|
data/lib/secure_headers.rb
CHANGED
|
@@ -9,6 +9,7 @@ require "secure_headers/headers/x_xss_protection"
|
|
|
9
9
|
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
|
+
require "secure_headers/headers/referrer_policy"
|
|
12
13
|
require "secure_headers/middleware"
|
|
13
14
|
require "secure_headers/railtie"
|
|
14
15
|
require "secure_headers/view_helper"
|
|
@@ -27,6 +28,7 @@ module SecureHeaders
|
|
|
27
28
|
ContentSecurityPolicy,
|
|
28
29
|
StrictTransportSecurity,
|
|
29
30
|
PublicKeyPins,
|
|
31
|
+
ReferrerPolicy,
|
|
30
32
|
XContentTypeOptions,
|
|
31
33
|
XDownloadOptions,
|
|
32
34
|
XFrameOptions,
|
data/lib/tasks/tasks.rake
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
INLINE_SCRIPT_REGEX = /(<script(\s*(?!src)([\w\-])+=([\"\'])[^\"\']+\4)*\s*>)(.*?)<\/script>/mx
|
|
2
|
-
INLINE_STYLE_REGEX = /(<style[^>]*>)(.*?)<\/style>/mx
|
|
3
|
-
INLINE_HASH_SCRIPT_HELPER_REGEX = /<%=\s?hashed_javascript_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx
|
|
4
|
-
INLINE_HASH_STYLE_HELPER_REGEX = /<%=\s?hashed_style_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx
|
|
1
|
+
INLINE_SCRIPT_REGEX = /(<script(\s*(?!src)([\w\-])+=([\"\'])[^\"\']+\4)*\s*>)(.*?)<\/script>/mx unless defined? INLINE_SCRIPT_REGEX
|
|
2
|
+
INLINE_STYLE_REGEX = /(<style[^>]*>)(.*?)<\/style>/mx unless defined? INLINE_STYLE_REGEX
|
|
3
|
+
INLINE_HASH_SCRIPT_HELPER_REGEX = /<%=\s?hashed_javascript_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx unless defined? INLINE_HASH_SCRIPT_HELPER_REGEX
|
|
4
|
+
INLINE_HASH_STYLE_HELPER_REGEX = /<%=\s?hashed_style_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx unless defined? INLINE_HASH_STYLE_HELPER_REGEX
|
|
5
5
|
|
|
6
6
|
namespace :secure_headers do
|
|
7
7
|
include SecureHeaders::HashHelper
|
|
@@ -54,6 +54,7 @@ namespace :secure_headers do
|
|
|
54
54
|
hashes
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
desc "Generate #{SecureHeaders::Configuration::HASH_CONFIG_FILE}"
|
|
57
58
|
task :generate_hashes do |t, args|
|
|
58
59
|
script_hashes = {
|
|
59
60
|
"scripts" => {},
|
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.4.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.'
|
|
@@ -24,7 +24,7 @@ module SecureHeaders
|
|
|
24
24
|
|
|
25
25
|
describe "#value" do
|
|
26
26
|
it "discards 'none' values if any other source expressions are present" do
|
|
27
|
-
csp = ContentSecurityPolicy.new(default_opts.merge(
|
|
27
|
+
csp = ContentSecurityPolicy.new(default_opts.merge(child_src: %w('self' 'none')))
|
|
28
28
|
expect(csp.value).not_to include("'none'")
|
|
29
29
|
end
|
|
30
30
|
|
|
@@ -71,42 +71,83 @@ module SecureHeaders
|
|
|
71
71
|
expect(csp.value).to eq("default-src example.org")
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
+
it "does add a boolean directive if the value is true" do
|
|
75
|
+
csp = ContentSecurityPolicy.new(default_src: ["https://example.org"], block_all_mixed_content: true, upgrade_insecure_requests: true)
|
|
76
|
+
expect(csp.value).to eq("default-src example.org; block-all-mixed-content; upgrade-insecure-requests")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it "does not add a boolean directive if the value is false" do
|
|
80
|
+
csp = ContentSecurityPolicy.new(default_src: ["https://example.org"], block_all_mixed_content: true, upgrade_insecure_requests: false)
|
|
81
|
+
expect(csp.value).to eq("default-src example.org; block-all-mixed-content")
|
|
82
|
+
end
|
|
83
|
+
|
|
74
84
|
it "deduplicates any source expressions" do
|
|
75
85
|
csp = ContentSecurityPolicy.new(default_src: %w(example.org example.org example.org))
|
|
76
86
|
expect(csp.value).to eq("default-src example.org")
|
|
77
87
|
end
|
|
78
88
|
|
|
89
|
+
it "emits a warning when using frame-src" do
|
|
90
|
+
expect(Kernel).to receive(:warn).with(/:frame_src is deprecated, use :child_src instead./)
|
|
91
|
+
ContentSecurityPolicy.new(default_src: %w('self'), frame_src: %w('self')).value
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it "emits a warning when child-src and frame-src are supplied but are not equal" do
|
|
95
|
+
expect(Kernel).to receive(:warn).with(/both :child_src and :frame_src supplied and do not match./)
|
|
96
|
+
ContentSecurityPolicy.new(default_src: %w('self'), child_src: %w(child-src.com), frame_src: %w(frame-src,com)).value
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it "will still set inconsistent child/frame-src values to be less surprising" do
|
|
100
|
+
expect(Kernel).to receive(:warn).at_least(:once)
|
|
101
|
+
firefox = ContentSecurityPolicy.new({default_src: %w('self'), child_src: %w(child-src.com), frame_src: %w(frame-src,com)}, USER_AGENTS[:firefox]).value
|
|
102
|
+
firefox_transitional = ContentSecurityPolicy.new({default_src: %w('self'), child_src: %w(child-src.com), frame_src: %w(frame-src,com)}, USER_AGENTS[:firefox46]).value
|
|
103
|
+
expect(firefox).not_to eq(firefox_transitional)
|
|
104
|
+
expect(firefox).to match(/frame-src/)
|
|
105
|
+
expect(firefox).not_to match(/child-src/)
|
|
106
|
+
expect(firefox_transitional).to match(/child-src/)
|
|
107
|
+
expect(firefox_transitional).not_to match(/frame-src/)
|
|
108
|
+
end
|
|
109
|
+
|
|
79
110
|
context "browser sniffing" do
|
|
80
111
|
let (:complex_opts) do
|
|
81
|
-
ContentSecurityPolicy::ALL_DIRECTIVES.each_with_object({}) do |directive, hash|
|
|
82
|
-
hash[directive] =
|
|
112
|
+
(ContentSecurityPolicy::ALL_DIRECTIVES - [:frame_src]).each_with_object({}) do |directive, hash|
|
|
113
|
+
hash[directive] = ["#{directive.to_s.gsub("_", "-")}.com"]
|
|
83
114
|
end.merge({
|
|
84
115
|
block_all_mixed_content: true,
|
|
85
116
|
upgrade_insecure_requests: true,
|
|
86
117
|
reflected_xss: "block",
|
|
87
|
-
script_src: %w(
|
|
118
|
+
script_src: %w(script-src.com),
|
|
88
119
|
script_nonce: 123456
|
|
89
120
|
})
|
|
90
121
|
end
|
|
91
122
|
|
|
92
123
|
it "does not filter any directives for Chrome" do
|
|
93
124
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:chrome])
|
|
94
|
-
expect(policy.value).to eq("default-src
|
|
125
|
+
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; plugin-types plugin-types.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
|
|
95
126
|
end
|
|
96
127
|
|
|
97
128
|
it "does not filter any directives for Opera" do
|
|
98
129
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:opera])
|
|
99
|
-
expect(policy.value).to eq("default-src
|
|
130
|
+
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; plugin-types plugin-types.com; sandbox sandbox.com; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
|
|
100
131
|
end
|
|
101
132
|
|
|
102
133
|
it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do
|
|
103
134
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox])
|
|
104
|
-
expect(policy.value).to eq("default-src
|
|
135
|
+
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")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it "filters blocked-all-mixed-content, frame-src, and plugin-types for firefox 46 and higher" do
|
|
139
|
+
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox46])
|
|
140
|
+
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.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")
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it "child-src value is copied to frame-src, adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for Edge" do
|
|
144
|
+
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:edge])
|
|
145
|
+
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")
|
|
105
146
|
end
|
|
106
147
|
|
|
107
|
-
it "adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for safari" do
|
|
148
|
+
it "child-src value is copied to frame-src, adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, nonce sources, hash sources, and plugin-types for safari" do
|
|
108
149
|
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari6])
|
|
109
|
-
expect(policy.value).to eq("default-src
|
|
150
|
+
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")
|
|
110
151
|
end
|
|
111
152
|
end
|
|
112
153
|
end
|
|
@@ -23,7 +23,8 @@ module SecureHeaders
|
|
|
23
23
|
# directive values: these values will directly translate into source directives
|
|
24
24
|
default_src: %w(https: 'self'),
|
|
25
25
|
frame_src: %w('self' *.twimg.com itunes.apple.com),
|
|
26
|
-
|
|
26
|
+
child_src: %w('self' *.twimg.com itunes.apple.com),
|
|
27
|
+
connect_src: %w(wss:),
|
|
27
28
|
font_src: %w('self' data:),
|
|
28
29
|
img_src: %w(mycdn.com data:),
|
|
29
30
|
media_src: %w(utoob.com),
|
|
@@ -31,7 +32,6 @@ module SecureHeaders
|
|
|
31
32
|
script_src: %w('self'),
|
|
32
33
|
style_src: %w('unsafe-inline'),
|
|
33
34
|
base_uri: %w('self'),
|
|
34
|
-
child_src: %w('self'),
|
|
35
35
|
form_action: %w('self' github.com),
|
|
36
36
|
frame_ancestors: %w('none'),
|
|
37
37
|
plugin_types: %w(application/x-shockwave-flash),
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
module SecureHeaders
|
|
4
|
+
describe ReferrerPolicy do
|
|
5
|
+
specify { expect(ReferrerPolicy.make_header).to eq([ReferrerPolicy::HEADER_NAME, "origin-when-cross-origin"]) }
|
|
6
|
+
specify { expect(ReferrerPolicy.make_header('no-referrer')).to eq([ReferrerPolicy::HEADER_NAME, "no-referrer"]) }
|
|
7
|
+
|
|
8
|
+
context "valid configuration values" do
|
|
9
|
+
it "accepts 'no-referrer'" do
|
|
10
|
+
expect do
|
|
11
|
+
ReferrerPolicy.validate_config!("no-referrer")
|
|
12
|
+
end.not_to raise_error
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "accepts 'no-referrer-when-downgrade'" do
|
|
16
|
+
expect do
|
|
17
|
+
ReferrerPolicy.validate_config!("no-referrer-when-downgrade")
|
|
18
|
+
end.not_to raise_error
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "accepts 'origin'" do
|
|
22
|
+
expect do
|
|
23
|
+
ReferrerPolicy.validate_config!("origin")
|
|
24
|
+
end.not_to raise_error
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "accepts 'origin-when-cross-origin'" do
|
|
28
|
+
expect do
|
|
29
|
+
ReferrerPolicy.validate_config!("origin-when-cross-origin")
|
|
30
|
+
end.not_to raise_error
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "accepts 'unsafe-url'" do
|
|
34
|
+
expect do
|
|
35
|
+
ReferrerPolicy.validate_config!("unsafe-url")
|
|
36
|
+
end.not_to raise_error
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "accepts nil" do
|
|
40
|
+
expect do
|
|
41
|
+
ReferrerPolicy.validate_config!(nil)
|
|
42
|
+
end.not_to raise_error
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
context 'invlaid configuration values' do
|
|
47
|
+
it "doesn't accept invalid values" do
|
|
48
|
+
expect do
|
|
49
|
+
ReferrerPolicy.validate_config!("open")
|
|
50
|
+
end.to raise_error(ReferrerPolicyConfigError)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -13,6 +13,24 @@ module SecureHeaders
|
|
|
13
13
|
Configuration.default
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
it "warns if the hpkp report-uri host is the same as the current host" do
|
|
17
|
+
report_host = "report-uri.io"
|
|
18
|
+
Configuration.default do |config|
|
|
19
|
+
config.hpkp = {
|
|
20
|
+
max_age: 10000000,
|
|
21
|
+
pins: [
|
|
22
|
+
{sha256: 'b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c'},
|
|
23
|
+
{sha256: '73a2c64f9545172c1195efb6616ca5f7afd1df6f245407cafb90de3998a1c97f'}
|
|
24
|
+
],
|
|
25
|
+
report_uri: "https://#{report_host}/example-hpkp"
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
expect(Kernel).to receive(:warn).with(Middleware::HPKP_SAME_HOST_WARNING)
|
|
30
|
+
|
|
31
|
+
middleware.call(Rack::MockRequest.env_for("https://#{report_host}", {}))
|
|
32
|
+
end
|
|
33
|
+
|
|
16
34
|
it "sets the headers" do
|
|
17
35
|
_, env = middleware.call(Rack::MockRequest.env_for("https://looocalhost", {}))
|
|
18
36
|
expect_default_values(env)
|
|
@@ -20,6 +20,13 @@ module SecureHeaders
|
|
|
20
20
|
end.to raise_error(Configuration::NotYetConfiguredError)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
it "raises and ArgumentError when referencing an override that has not been set" do
|
|
24
|
+
expect do
|
|
25
|
+
Configuration.default
|
|
26
|
+
SecureHeaders.use_secure_headers_override(request, :missing)
|
|
27
|
+
end.to raise_error(ArgumentError)
|
|
28
|
+
end
|
|
29
|
+
|
|
23
30
|
describe "#header_hash_for" do
|
|
24
31
|
it "allows you to opt out of individual headers via API" do
|
|
25
32
|
Configuration.default
|
|
@@ -83,8 +90,7 @@ module SecureHeaders
|
|
|
83
90
|
config = Configuration.default do |config|
|
|
84
91
|
config.csp = {
|
|
85
92
|
default_src: %w('self'),
|
|
86
|
-
child_src: %w('self')
|
|
87
|
-
frame_src: %w('self')
|
|
93
|
+
child_src: %w('self')
|
|
88
94
|
}
|
|
89
95
|
end
|
|
90
96
|
firefox_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:firefox]))
|
|
@@ -95,6 +101,8 @@ module SecureHeaders
|
|
|
95
101
|
SecureHeaders.override_content_security_policy_directives(firefox_request, script_src: %w('self'))
|
|
96
102
|
|
|
97
103
|
hash = SecureHeaders.header_hash_for(firefox_request)
|
|
104
|
+
|
|
105
|
+
# child-src is translated to frame-src
|
|
98
106
|
expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; frame-src 'self'; script-src 'self'")
|
|
99
107
|
end
|
|
100
108
|
|
|
@@ -305,6 +313,14 @@ module SecureHeaders
|
|
|
305
313
|
end.to raise_error(XPCDPConfigError)
|
|
306
314
|
end
|
|
307
315
|
|
|
316
|
+
it "validates your referrer_policy config upon configuration" do
|
|
317
|
+
expect do
|
|
318
|
+
Configuration.default do |config|
|
|
319
|
+
config.referrer_policy = "lol"
|
|
320
|
+
end
|
|
321
|
+
end.to raise_error(ReferrerPolicyConfigError)
|
|
322
|
+
end
|
|
323
|
+
|
|
308
324
|
it "validates your hpkp config upon configuration" do
|
|
309
325
|
expect do
|
|
310
326
|
Configuration.default do |config|
|
|
@@ -312,6 +328,14 @@ module SecureHeaders
|
|
|
312
328
|
end
|
|
313
329
|
end.to raise_error(PublicKeyPinsConfigError)
|
|
314
330
|
end
|
|
331
|
+
|
|
332
|
+
it "validates your cookies config upon configuration" do
|
|
333
|
+
expect do
|
|
334
|
+
Configuration.default do |config|
|
|
335
|
+
config.cookies = { secure: "lol" }
|
|
336
|
+
end
|
|
337
|
+
end.to raise_error(CookiesConfigError)
|
|
338
|
+
end
|
|
315
339
|
end
|
|
316
340
|
end
|
|
317
341
|
end
|
data/spec/spec_helper.rb
CHANGED
|
@@ -11,7 +11,9 @@ require File.join(File.dirname(__FILE__), '..', 'lib', 'secure_headers')
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
USER_AGENTS = {
|
|
14
|
-
|
|
14
|
+
edge: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246",
|
|
15
|
+
firefox: "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:14.0) Gecko/20100101 Firefox/14.0.1",
|
|
16
|
+
firefox46: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:46.0) Gecko/20100101 Firefox/46.0",
|
|
15
17
|
chrome: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.56 Safari/536.5',
|
|
16
18
|
ie: 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/5.0)',
|
|
17
19
|
opera: 'Opera/9.80 (Windows NT 6.1; U; es-ES) Presto/2.9.181 Version/12.00',
|
|
@@ -30,6 +32,7 @@ def expect_default_values(hash)
|
|
|
30
32
|
expect(hash[SecureHeaders::XXssProtection::HEADER_NAME]).to eq(SecureHeaders::XXssProtection::DEFAULT_VALUE)
|
|
31
33
|
expect(hash[SecureHeaders::XContentTypeOptions::HEADER_NAME]).to eq(SecureHeaders::XContentTypeOptions::DEFAULT_VALUE)
|
|
32
34
|
expect(hash[SecureHeaders::XPermittedCrossDomainPolicies::HEADER_NAME]).to eq(SecureHeaders::XPermittedCrossDomainPolicies::DEFAULT_VALUE)
|
|
35
|
+
expect(hash[SecureHeaders::ReferrerPolicy::HEADER_NAME]).to be_nil
|
|
33
36
|
end
|
|
34
37
|
|
|
35
38
|
module SecureHeaders
|
data/upgrading-to-3-0.md
CHANGED
|
@@ -3,16 +3,19 @@
|
|
|
3
3
|
Changes
|
|
4
4
|
==
|
|
5
5
|
|
|
6
|
-
| What
|
|
7
|
-
|
|
8
|
-
| Global configuration
|
|
9
|
-
| All headers besides HPKP and CSP
|
|
10
|
-
| CSP directive values
|
|
11
|
-
| CSP Nonce values in views
|
|
12
|
-
|
|
|
13
|
-
| `
|
|
14
|
-
|
|
|
15
|
-
|
|
|
6
|
+
| What | < = 2.x | >= 3.0 |
|
|
7
|
+
| ---------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
8
|
+
| Global configuration | `SecureHeaders::Configuration.configure` block | `SecureHeaders::Configuration.default` block |
|
|
9
|
+
| All headers besides HPKP and CSP | Accept hashes as config values | Must be strings (validated during configuration) |
|
|
10
|
+
| CSP directive values | Accepted space delimited strings OR arrays of strings | Must be arrays of strings |
|
|
11
|
+
| CSP Nonce values in views | `@content_security_policy_nonce` | `content_security_policy_nonce(:script)` or `content_security_policy_nonce(:style)` |
|
|
12
|
+
| nonce is no longer a source expression | `config.csp = "'self' 'nonce'"` | Remove `'nonce'` from source expression and use [nonce helpers](https://github.com/twitter/secureheaders#nonce). |
|
|
13
|
+
| `self`/`none` source expressions | Could be `self` / `none` / `'self'` / `'none'` | Must be `'self'` or `'none'` |
|
|
14
|
+
| `inline` / `eval` source expressions | Could be `inline`, `eval`, `'unsafe-inline'`, or `'unsafe-eval'` | Must be `'unsafe-eval'` or `'unsafe-inline'` |
|
|
15
|
+
| Per-action configuration | Override [`def secure_header_options_for(header, options)`](https://github.com/twitter/secureheaders/commit/bb9ebc6c12a677aad29af8e0f08ffd1def56efec#diff-04c6e90faac2675aa89e2176d2eec7d8R111) | Use [named overrides](https://github.com/twitter/secureheaders#named-overrides) or [per-action helpers](https://github.com/twitter/secureheaders#per-action-configuration) |
|
|
16
|
+
| CSP/HPKP use `report_only` config that defaults to false | `enforce: false` | `report_only: false` |
|
|
17
|
+
| Schemes in source expressions | Schemes were not stripped | Schemes are stripped by default to discourage mixed content. Setting `preserve_schemes: true` will revert to previous behavior |
|
|
18
|
+
| Opting out of default configuration | `skip_before_filter :set_x_download_options_header` or `config.x_download_options = false` | Within default block: `config.x_download_options = SecureHeaders::OPT_OUT` |
|
|
16
19
|
|
|
17
20
|
Migrating to 3.x from <= 2.x
|
|
18
21
|
==
|
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.4.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: 2016-
|
|
11
|
+
date: 2016-07-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rake
|
|
@@ -45,6 +45,8 @@ executables: []
|
|
|
45
45
|
extensions: []
|
|
46
46
|
extra_rdoc_files: []
|
|
47
47
|
files:
|
|
48
|
+
- ".github/ISSUE_TEMPLATE.md"
|
|
49
|
+
- ".github/PULL_REQUEST_TEMPLATE.md"
|
|
48
50
|
- ".gitignore"
|
|
49
51
|
- ".rspec"
|
|
50
52
|
- ".ruby-gemset"
|
|
@@ -63,6 +65,7 @@ files:
|
|
|
63
65
|
- lib/secure_headers/headers/cookie.rb
|
|
64
66
|
- lib/secure_headers/headers/policy_management.rb
|
|
65
67
|
- lib/secure_headers/headers/public_key_pins.rb
|
|
68
|
+
- lib/secure_headers/headers/referrer_policy.rb
|
|
66
69
|
- lib/secure_headers/headers/strict_transport_security.rb
|
|
67
70
|
- lib/secure_headers/headers/x_content_type_options.rb
|
|
68
71
|
- lib/secure_headers/headers/x_download_options.rb
|
|
@@ -80,6 +83,7 @@ files:
|
|
|
80
83
|
- spec/lib/secure_headers/headers/cookie_spec.rb
|
|
81
84
|
- spec/lib/secure_headers/headers/policy_management_spec.rb
|
|
82
85
|
- spec/lib/secure_headers/headers/public_key_pins_spec.rb
|
|
86
|
+
- spec/lib/secure_headers/headers/referrer_policy_spec.rb
|
|
83
87
|
- spec/lib/secure_headers/headers/strict_transport_security_spec.rb
|
|
84
88
|
- spec/lib/secure_headers/headers/x_content_type_options_spec.rb
|
|
85
89
|
- spec/lib/secure_headers/headers/x_download_options_spec.rb
|
|
@@ -122,6 +126,7 @@ test_files:
|
|
|
122
126
|
- spec/lib/secure_headers/headers/cookie_spec.rb
|
|
123
127
|
- spec/lib/secure_headers/headers/policy_management_spec.rb
|
|
124
128
|
- spec/lib/secure_headers/headers/public_key_pins_spec.rb
|
|
129
|
+
- spec/lib/secure_headers/headers/referrer_policy_spec.rb
|
|
125
130
|
- spec/lib/secure_headers/headers/strict_transport_security_spec.rb
|
|
126
131
|
- spec/lib/secure_headers/headers/x_content_type_options_spec.rb
|
|
127
132
|
- spec/lib/secure_headers/headers/x_download_options_spec.rb
|