secure_headers 3.4.0 → 3.5.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/.ruby-version +1 -1
- data/CHANGELOG.md +66 -0
- data/Gemfile +1 -0
- data/README.md +89 -9
- data/lib/secure_headers/configuration.rb +68 -36
- data/lib/secure_headers/headers/content_security_policy.rb +62 -36
- data/lib/secure_headers/headers/content_security_policy_config.rb +128 -0
- data/lib/secure_headers/headers/policy_management.rb +48 -30
- data/lib/secure_headers/view_helper.rb +8 -0
- data/lib/secure_headers.rb +131 -55
- data/secure_headers.gemspec +1 -1
- data/spec/lib/secure_headers/configuration_spec.rb +5 -5
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +2 -2
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +25 -34
- data/spec/lib/secure_headers/middleware_spec.rb +13 -1
- data/spec/lib/secure_headers/view_helpers_spec.rb +19 -6
- data/spec/lib/secure_headers_spec.rb +281 -40
- data/spec/spec_helper.rb +3 -2
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f47871f10c97316165d3f4230ddae85935d9a736
|
|
4
|
+
data.tar.gz: f5777312e07643e054d48b4121ba30a905aab4dd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8516799304bf21ea8d67aace99325df8c34d1e3d72082b44ad4a80c658ddc9cee6e4a04fc8202ac062044c0abbc91ce870fc26a472960f390fe359670f7362cf
|
|
7
|
+
data.tar.gz: d4fc7f5482fc8e904ce3ea80340da5c5426b87c88bbb8f669af9e050b212a1189b997de0c3c6bdc9a3b4df58c69efa54598ea40caaf56eb9bc69a80722029f8c
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.2.
|
|
1
|
+
2.2.5
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,69 @@
|
|
|
1
|
+
## 3.5.0
|
|
2
|
+
|
|
3
|
+
This release adds support for setting two CSP headers (enforced/report-only) and management around them.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
## 3.4.1 Named Appends
|
|
7
|
+
|
|
8
|
+
### Small bugfix
|
|
9
|
+
|
|
10
|
+
If your CSP did not define a script/style-src and you tried to use a script/style nonce, the nonce would be added to the page but it would not be added to the CSP. A workaround is to define a script/style src but now it should add the missing directive (and populate it with the default-src).
|
|
11
|
+
|
|
12
|
+
### Named Appends
|
|
13
|
+
|
|
14
|
+
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.
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
def show
|
|
18
|
+
if include_widget?
|
|
19
|
+
@widget = widget.render
|
|
20
|
+
use_content_security_policy_named_append(:widget_partial)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
SecureHeaders::Configuration.named_append(:widget_partial) do |request|
|
|
26
|
+
if request.controller_instance.current_user.in_test_bucket?
|
|
27
|
+
SecureHeaders.override_x_frame_options(request, "DENY")
|
|
28
|
+
{ child_src: %w(beta.thirdpartyhost.com) }
|
|
29
|
+
else
|
|
30
|
+
{ child_src: %w(thirdpartyhost.com) }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
You can use as many named appends as you would like per request, but be careful because order of inclusion matters. Consider the following:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
SecureHeader::Configuration.default do |config|
|
|
39
|
+
config.csp = { default_src: %w('self')}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
SecureHeaders::Configuration.named_append(:A) do |request|
|
|
43
|
+
{ default_src: %w(myhost.com) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
SecureHeaders::Configuration.named_append(:B) do |request|
|
|
47
|
+
{ script_src: %w('unsafe-eval') }
|
|
48
|
+
end
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
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.):
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
def index
|
|
55
|
+
use_content_security_policy_named_append(:A)
|
|
56
|
+
use_content_security_policy_named_append(:B)
|
|
57
|
+
# produces default-src 'self' myhost.com; script-src 'self' myhost.com 'unsafe-eval';
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def show
|
|
61
|
+
use_content_security_policy_named_append(:B)
|
|
62
|
+
use_content_security_policy_named_append(:A)
|
|
63
|
+
# produces default-src 'self' myhost.com; script-src 'self' 'unsafe-eval';
|
|
64
|
+
end
|
|
65
|
+
```
|
|
66
|
+
|
|
1
67
|
## 3.4.0 the frame-src/child-src transition for Firefox.
|
|
2
68
|
|
|
3
69
|
Handle the `child-src`/`frame-src` transition semi-intelligently across versions. I think the code best descibes the behavior here:
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Secure Headers [](http://travis-ci.org/twitter/secureheaders) [](https://codeclimate.com/github/twitter/secureheaders) [](https://coveralls.io/r/twitter/secureheaders)
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
**The 3.x branch was recently merged**. See the [upgrading to 3.x doc](upgrading-to-3-0.md) for instructions on how to upgrade including the differences and benefits of using the 3.x branch.
|
|
@@ -20,10 +20,6 @@ It can also mark all http cookies with the Secure, HttpOnly and SameSite attribu
|
|
|
20
20
|
|
|
21
21
|
`secure_headers` is a library with a global config, per request overrides, and rack middleware that enables you customize your application settings.
|
|
22
22
|
|
|
23
|
-
## Use
|
|
24
|
-
|
|
25
|
-
`gem install secure_headers`
|
|
26
|
-
|
|
27
23
|
## Configuration
|
|
28
24
|
|
|
29
25
|
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.
|
|
@@ -36,7 +32,7 @@ SecureHeaders::Configuration.default do |config|
|
|
|
36
32
|
secure: true, # mark all cookies as "Secure"
|
|
37
33
|
httponly: true, # mark all cookies as "HttpOnly"
|
|
38
34
|
samesite: {
|
|
39
|
-
|
|
35
|
+
lax: true # mark all cookies as SameSite=lax
|
|
40
36
|
}
|
|
41
37
|
}
|
|
42
38
|
config.hsts = "max-age=#{20.years.to_i}; includeSubdomains; preload"
|
|
@@ -48,7 +44,7 @@ SecureHeaders::Configuration.default do |config|
|
|
|
48
44
|
config.referrer_policy = "origin-when-cross-origin"
|
|
49
45
|
config.csp = {
|
|
50
46
|
# "meta" values. these will shaped the header, but the values are not included in the header.
|
|
51
|
-
report_only: true, # default: false
|
|
47
|
+
report_only: true, # default: false [DEPRECATED from 3.5.0: instead, configure csp_report_only]
|
|
52
48
|
preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content.
|
|
53
49
|
|
|
54
50
|
# directive values: these values will directly translate into source directives
|
|
@@ -69,6 +65,11 @@ SecureHeaders::Configuration.default do |config|
|
|
|
69
65
|
upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/
|
|
70
66
|
report_uri: %w(https://report-uri.io/example-csp)
|
|
71
67
|
}
|
|
68
|
+
# This is available only from 3.5.0; use the `report_only: true` setting for 3.4.1 and below.
|
|
69
|
+
config.csp_report_only = config.csp.merge({
|
|
70
|
+
img_src: %w(somewhereelse.com),
|
|
71
|
+
report_uri: %w(https://report-uri.io/example-csp-report-only)
|
|
72
|
+
})
|
|
72
73
|
config.hpkp = {
|
|
73
74
|
report_only: false,
|
|
74
75
|
max_age: 60.days.to_i,
|
|
@@ -92,7 +93,86 @@ use SecureHeaders::Middleware
|
|
|
92
93
|
|
|
93
94
|
## Default values
|
|
94
95
|
|
|
95
|
-
All headers except for PublicKeyPins have a default value. See the [corresponding classes for their defaults](https://github.com/twitter/secureheaders/tree/master/lib/secure_headers/headers).
|
|
96
|
+
All headers except for PublicKeyPins have a default value. See the [corresponding classes for their defaults](https://github.com/twitter/secureheaders/tree/master/lib/secure_headers/headers). The default set of headers is:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
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'
|
|
100
|
+
Strict-Transport-Security: max-age=631138519
|
|
101
|
+
X-Content-Type-Options: nosniff
|
|
102
|
+
X-Download-Options: noopen
|
|
103
|
+
X-Frame-Options: sameorigin
|
|
104
|
+
X-Permitted-Cross-Domain-Policies: none
|
|
105
|
+
X-Xss-Protection: 1; mode=block
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Default CSP
|
|
109
|
+
|
|
110
|
+
By default, the above CSP will be applied to all requests. If you **only** want to set a Report-Only header, opt-out of the default enforced header for clarity. The configuration will assume that if you only supply `csp_report_only` that you intended to opt-out of `csp` but that's for the sake of backwards compatibility and it will be removed in the future.
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
Configuration.default do |config|
|
|
114
|
+
config.csp = SecureHeaders::OPT_OUT # If this line is omitted, we will assume you meant to opt out.
|
|
115
|
+
config.csp_report_only = {
|
|
116
|
+
default_src: %w('self')
|
|
117
|
+
}
|
|
118
|
+
end
|
|
119
|
+
```
|
|
120
|
+
|
|
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
|
+
|
|
96
176
|
|
|
97
177
|
## Named overrides
|
|
98
178
|
|
|
@@ -125,7 +205,7 @@ end
|
|
|
125
205
|
|
|
126
206
|
class MyController < ApplicationController
|
|
127
207
|
def index
|
|
128
|
-
# Produces default-src 'self'; script-src example.org otherdomain.
|
|
208
|
+
# Produces default-src 'self'; script-src example.org otherdomain.com
|
|
129
209
|
use_secure_headers_override(:script_from_otherdomain_com)
|
|
130
210
|
end
|
|
131
211
|
|
|
@@ -46,6 +46,17 @@ module SecureHeaders
|
|
|
46
46
|
@configurations[name]
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
+
def named_appends(name)
|
|
50
|
+
@appends ||= {}
|
|
51
|
+
@appends[name]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def named_append(name, target = nil, &block)
|
|
55
|
+
@appends ||= {}
|
|
56
|
+
raise "Provide a configuration block" unless block_given?
|
|
57
|
+
@appends[name] = block
|
|
58
|
+
end
|
|
59
|
+
|
|
49
60
|
private
|
|
50
61
|
|
|
51
62
|
# Private: add a valid configuration to the global set of named configs.
|
|
@@ -74,7 +85,6 @@ module SecureHeaders
|
|
|
74
85
|
ALL_HEADER_CLASSES.each do |klass|
|
|
75
86
|
config.send("#{klass::CONFIG_KEY}=", OPT_OUT)
|
|
76
87
|
end
|
|
77
|
-
config.dynamic_csp = OPT_OUT
|
|
78
88
|
end
|
|
79
89
|
|
|
80
90
|
add_configuration(NOOP_CONFIGURATION, noop_config)
|
|
@@ -83,6 +93,7 @@ module SecureHeaders
|
|
|
83
93
|
# Public: perform a basic deep dup. The shallow copy provided by dup/clone
|
|
84
94
|
# can lead to modifying parent objects.
|
|
85
95
|
def deep_copy(config)
|
|
96
|
+
return unless config
|
|
86
97
|
config.each_with_object({}) do |(key, value), hash|
|
|
87
98
|
hash[key] = if value.is_a?(Array)
|
|
88
99
|
value.dup
|
|
@@ -103,13 +114,11 @@ module SecureHeaders
|
|
|
103
114
|
end
|
|
104
115
|
end
|
|
105
116
|
|
|
106
|
-
attr_accessor :dynamic_csp
|
|
107
|
-
|
|
108
117
|
attr_writer :hsts, :x_frame_options, :x_content_type_options,
|
|
109
118
|
:x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies,
|
|
110
119
|
:referrer_policy
|
|
111
120
|
|
|
112
|
-
attr_reader :cached_headers, :csp, :cookies, :hpkp, :hpkp_report_host
|
|
121
|
+
attr_reader :cached_headers, :csp, :cookies, :csp_report_only, :hpkp, :hpkp_report_host
|
|
113
122
|
|
|
114
123
|
HASH_CONFIG_FILE = ENV["secure_headers_generated_hashes_file"] || "config/secure_headers_generated_hashes.yml"
|
|
115
124
|
if File.exists?(HASH_CONFIG_FILE)
|
|
@@ -121,7 +130,8 @@ module SecureHeaders
|
|
|
121
130
|
def initialize(&block)
|
|
122
131
|
self.hpkp = OPT_OUT
|
|
123
132
|
self.referrer_policy = OPT_OUT
|
|
124
|
-
self.csp =
|
|
133
|
+
self.csp = ContentSecurityPolicyConfig.new(ContentSecurityPolicyConfig::DEFAULT)
|
|
134
|
+
self.csp_report_only = OPT_OUT
|
|
125
135
|
instance_eval &block if block_given?
|
|
126
136
|
end
|
|
127
137
|
|
|
@@ -130,9 +140,9 @@ module SecureHeaders
|
|
|
130
140
|
# Returns a deep-dup'd copy of this configuration.
|
|
131
141
|
def dup
|
|
132
142
|
copy = self.class.new
|
|
133
|
-
copy.cookies = @cookies
|
|
134
|
-
copy.csp =
|
|
135
|
-
copy.
|
|
143
|
+
copy.cookies = self.class.send(:deep_copy_if_hash, @cookies)
|
|
144
|
+
copy.csp = @csp.dup if @csp
|
|
145
|
+
copy.csp_report_only = @csp_report_only.dup if @csp_report_only
|
|
136
146
|
copy.cached_headers = self.class.send(:deep_copy_if_hash, @cached_headers)
|
|
137
147
|
copy.x_content_type_options = @x_content_type_options
|
|
138
148
|
copy.hsts = @hsts
|
|
@@ -148,9 +158,6 @@ module SecureHeaders
|
|
|
148
158
|
|
|
149
159
|
def opt_out(header)
|
|
150
160
|
send("#{header}=", OPT_OUT)
|
|
151
|
-
if header == CSP::CONFIG_KEY
|
|
152
|
-
dynamic_csp = OPT_OUT
|
|
153
|
-
end
|
|
154
161
|
self.cached_headers.delete(header)
|
|
155
162
|
end
|
|
156
163
|
|
|
@@ -159,20 +166,6 @@ module SecureHeaders
|
|
|
159
166
|
self.cached_headers[XFrameOptions::CONFIG_KEY] = XFrameOptions.make_header(value)
|
|
160
167
|
end
|
|
161
168
|
|
|
162
|
-
# Public: generated cached headers for a specific user agent.
|
|
163
|
-
def rebuild_csp_header_cache!(user_agent)
|
|
164
|
-
self.cached_headers[CSP::CONFIG_KEY] = {}
|
|
165
|
-
unless current_csp == OPT_OUT
|
|
166
|
-
user_agent = UserAgent.parse(user_agent)
|
|
167
|
-
variation = CSP.ua_to_variation(user_agent)
|
|
168
|
-
self.cached_headers[CSP::CONFIG_KEY][variation] = CSP.make_header(current_csp, user_agent)
|
|
169
|
-
end
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def current_csp
|
|
173
|
-
@dynamic_csp || @csp
|
|
174
|
-
end
|
|
175
|
-
|
|
176
169
|
# Public: validates all configurations values.
|
|
177
170
|
#
|
|
178
171
|
# Raises various configuration errors if any invalid config is detected.
|
|
@@ -181,6 +174,7 @@ module SecureHeaders
|
|
|
181
174
|
def validate_config!
|
|
182
175
|
StrictTransportSecurity.validate_config!(@hsts)
|
|
183
176
|
ContentSecurityPolicy.validate_config!(@csp)
|
|
177
|
+
ContentSecurityPolicy.validate_config!(@csp_report_only)
|
|
184
178
|
ReferrerPolicy.validate_config!(@referrer_policy)
|
|
185
179
|
XFrameOptions.validate_config!(@x_frame_options)
|
|
186
180
|
XContentTypeOptions.validate_config!(@x_content_type_options)
|
|
@@ -196,16 +190,50 @@ module SecureHeaders
|
|
|
196
190
|
@cookies = (@cookies || {}).merge(secure: secure_cookies)
|
|
197
191
|
end
|
|
198
192
|
|
|
199
|
-
protected
|
|
200
|
-
|
|
201
193
|
def csp=(new_csp)
|
|
202
|
-
if
|
|
203
|
-
|
|
194
|
+
if new_csp.respond_to?(:opt_out?)
|
|
195
|
+
@csp = new_csp.dup
|
|
196
|
+
else
|
|
197
|
+
if new_csp[:report_only]
|
|
198
|
+
# Deprecated configuration implies that CSPRO should be set, CSP should not - so opt out
|
|
199
|
+
Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#csp=` was supplied a config with report_only: true. Use #csp_report_only="
|
|
200
|
+
@csp = OPT_OUT
|
|
201
|
+
self.csp_report_only = new_csp
|
|
202
|
+
else
|
|
203
|
+
@csp = ContentSecurityPolicyConfig.new(new_csp)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Configures the Content-Security-Policy-Report-Only header. `new_csp` cannot
|
|
209
|
+
# contain `report_only: false` or an error will be raised.
|
|
210
|
+
#
|
|
211
|
+
# NOTE: if csp has not been configured/has the default value when
|
|
212
|
+
# configuring csp_report_only, the code will assume you mean to only use
|
|
213
|
+
# report-only mode and you will be opted-out of enforce mode.
|
|
214
|
+
def csp_report_only=(new_csp)
|
|
215
|
+
@csp_report_only = begin
|
|
216
|
+
if new_csp.is_a?(ContentSecurityPolicyConfig)
|
|
217
|
+
new_csp.make_report_only
|
|
218
|
+
elsif new_csp.respond_to?(:opt_out?)
|
|
219
|
+
new_csp.dup
|
|
220
|
+
else
|
|
221
|
+
if new_csp[:report_only] == false # nil is a valid value on which we do not want to raise
|
|
222
|
+
raise ContentSecurityPolicyConfigError, "`#csp_report_only=` was supplied a config with report_only: false. Use #csp="
|
|
223
|
+
else
|
|
224
|
+
ContentSecurityPolicyReportOnlyConfig.new(new_csp)
|
|
225
|
+
end
|
|
226
|
+
end
|
|
204
227
|
end
|
|
205
228
|
|
|
206
|
-
@csp
|
|
229
|
+
if !@csp_report_only.opt_out? && @csp.to_h == ContentSecurityPolicyConfig::DEFAULT
|
|
230
|
+
Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#csp_report_only=` was configured before `#csp=`. It is assumed you intended to opt out of `#csp=` so be sure to add `config.csp = SecureHeaders::OPT_OUT` to your config. Ensure that #csp_report_only is configured after #csp="
|
|
231
|
+
@csp = OPT_OUT
|
|
232
|
+
end
|
|
207
233
|
end
|
|
208
234
|
|
|
235
|
+
protected
|
|
236
|
+
|
|
209
237
|
def cookies=(cookies)
|
|
210
238
|
@cookies = cookies
|
|
211
239
|
end
|
|
@@ -258,12 +286,16 @@ module SecureHeaders
|
|
|
258
286
|
#
|
|
259
287
|
# Returns nothing
|
|
260
288
|
def generate_csp_headers(headers)
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
289
|
+
generate_csp_headers_for_config(headers, ContentSecurityPolicyConfig::CONFIG_KEY, self.csp)
|
|
290
|
+
generate_csp_headers_for_config(headers, ContentSecurityPolicyReportOnlyConfig::CONFIG_KEY, self.csp_report_only)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def generate_csp_headers_for_config(headers, header_key, csp_config)
|
|
294
|
+
unless csp_config.opt_out?
|
|
295
|
+
headers[header_key] = {}
|
|
296
|
+
ContentSecurityPolicy::VARIATIONS.each do |name, _|
|
|
297
|
+
csp = ContentSecurityPolicy.make_header(csp_config, UserAgent.parse(name))
|
|
298
|
+
headers[header_key][name] = csp.freeze
|
|
267
299
|
end
|
|
268
300
|
end
|
|
269
301
|
end
|
|
@@ -1,37 +1,44 @@
|
|
|
1
1
|
require_relative 'policy_management'
|
|
2
|
+
require_relative 'content_security_policy_config'
|
|
2
3
|
require 'useragent'
|
|
3
4
|
|
|
4
5
|
module SecureHeaders
|
|
5
|
-
class ContentSecurityPolicyConfigError < StandardError; end
|
|
6
6
|
class ContentSecurityPolicy
|
|
7
7
|
include PolicyManagement
|
|
8
8
|
|
|
9
9
|
# constants to be used for version-specific UA sniffing
|
|
10
10
|
VERSION_46 = ::UserAgent::Version.new("46")
|
|
11
|
+
VERSION_10 = ::UserAgent::Version.new("10")
|
|
11
12
|
|
|
12
13
|
def initialize(config = nil, user_agent = OTHER)
|
|
13
|
-
@config =
|
|
14
|
+
@config = if config.is_a?(Hash)
|
|
15
|
+
if config[:report_only]
|
|
16
|
+
ContentSecurityPolicyReportOnlyConfig.new(config || DEFAULT_CONFIG)
|
|
17
|
+
else
|
|
18
|
+
ContentSecurityPolicyConfig.new(config || DEFAULT_CONFIG)
|
|
19
|
+
end
|
|
20
|
+
elsif config.nil?
|
|
21
|
+
ContentSecurityPolicyConfig.new(DEFAULT_CONFIG)
|
|
22
|
+
else
|
|
23
|
+
config
|
|
24
|
+
end
|
|
25
|
+
|
|
14
26
|
@parsed_ua = if user_agent.is_a?(UserAgent::Browsers::Base)
|
|
15
27
|
user_agent
|
|
16
28
|
else
|
|
17
29
|
UserAgent.parse(user_agent)
|
|
18
30
|
end
|
|
19
|
-
normalize_child_frame_src
|
|
20
|
-
@
|
|
21
|
-
@
|
|
22
|
-
@
|
|
23
|
-
@style_nonce = @config[:style_nonce]
|
|
31
|
+
@frame_src = normalize_child_frame_src
|
|
32
|
+
@preserve_schemes = @config.preserve_schemes
|
|
33
|
+
@script_nonce = @config.script_nonce
|
|
34
|
+
@style_nonce = @config.style_nonce
|
|
24
35
|
end
|
|
25
36
|
|
|
26
37
|
##
|
|
27
38
|
# Returns the name to use for the header. Either "Content-Security-Policy" or
|
|
28
39
|
# "Content-Security-Policy-Report-Only"
|
|
29
40
|
def name
|
|
30
|
-
|
|
31
|
-
REPORT_ONLY
|
|
32
|
-
else
|
|
33
|
-
HEADER_NAME
|
|
34
|
-
end
|
|
41
|
+
@config.class.const_get(:HEADER_NAME)
|
|
35
42
|
end
|
|
36
43
|
|
|
37
44
|
##
|
|
@@ -49,16 +56,16 @@ module SecureHeaders
|
|
|
49
56
|
# frame-src is deprecated, child-src is being implemented. They are
|
|
50
57
|
# very similar and in most cases, the same value can be used for both.
|
|
51
58
|
def normalize_child_frame_src
|
|
52
|
-
if @config
|
|
59
|
+
if @config.frame_src && @config.child_src && @config.frame_src != @config.child_src
|
|
53
60
|
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
|
|
55
|
-
Kernel.warn("#{Kernel.caller.first}: [DEPRECATION] :frame_src is deprecated, use :child_src instead. Provided: #{@config
|
|
61
|
+
elsif @config.frame_src
|
|
62
|
+
Kernel.warn("#{Kernel.caller.first}: [DEPRECATION] :frame_src is deprecated, use :child_src instead. Provided: #{@config.frame_src}.")
|
|
56
63
|
end
|
|
57
64
|
|
|
58
65
|
if supported_directives.include?(:child_src)
|
|
59
|
-
@config
|
|
66
|
+
@config.child_src || @config.frame_src
|
|
60
67
|
else
|
|
61
|
-
@config
|
|
68
|
+
@config.frame_src || @config.child_src
|
|
62
69
|
end
|
|
63
70
|
end
|
|
64
71
|
|
|
@@ -73,9 +80,9 @@ module SecureHeaders
|
|
|
73
80
|
directives.map do |directive_name|
|
|
74
81
|
case DIRECTIVE_VALUE_TYPES[directive_name]
|
|
75
82
|
when :boolean
|
|
76
|
-
symbol_to_hyphen_case(directive_name) if @config
|
|
83
|
+
symbol_to_hyphen_case(directive_name) if @config.directive_value(directive_name)
|
|
77
84
|
when :string
|
|
78
|
-
[symbol_to_hyphen_case(directive_name), @config
|
|
85
|
+
[symbol_to_hyphen_case(directive_name), @config.directive_value(directive_name)].join(" ")
|
|
79
86
|
else
|
|
80
87
|
build_directive(directive_name)
|
|
81
88
|
end
|
|
@@ -88,11 +95,19 @@ module SecureHeaders
|
|
|
88
95
|
#
|
|
89
96
|
# Returns a string representing a directive.
|
|
90
97
|
def build_directive(directive)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
98
|
+
source_list = case directive
|
|
99
|
+
when :child_src
|
|
100
|
+
if supported_directives.include?(:child_src)
|
|
101
|
+
@frame_src
|
|
102
|
+
end
|
|
103
|
+
when :frame_src
|
|
104
|
+
unless supported_directives.include?(:child_src)
|
|
105
|
+
@frame_src
|
|
106
|
+
end
|
|
107
|
+
else
|
|
108
|
+
@config.directive_value(directive)
|
|
109
|
+
end
|
|
110
|
+
return unless source_list && source_list.any?
|
|
96
111
|
normalized_source_list = minify_source_list(directive, source_list)
|
|
97
112
|
[symbol_to_hyphen_case(directive), normalized_source_list].join(" ")
|
|
98
113
|
end
|
|
@@ -101,16 +116,17 @@ module SecureHeaders
|
|
|
101
116
|
# If a directive contains 'none' but has other values, 'none' is ommitted.
|
|
102
117
|
# Schemes are stripped (see http://www.w3.org/TR/CSP2/#match-source-expression)
|
|
103
118
|
def minify_source_list(directive, source_list)
|
|
119
|
+
source_list = source_list.compact
|
|
104
120
|
if source_list.include?(STAR)
|
|
105
121
|
keep_wildcard_sources(source_list)
|
|
106
122
|
else
|
|
107
|
-
populate_nonces
|
|
108
|
-
reject_all_values_if_none
|
|
123
|
+
source_list = populate_nonces(directive, source_list)
|
|
124
|
+
source_list = reject_all_values_if_none(source_list)
|
|
109
125
|
|
|
110
126
|
unless directive == REPORT_URI || @preserve_schemes
|
|
111
|
-
strip_source_schemes
|
|
127
|
+
source_list = strip_source_schemes(source_list)
|
|
112
128
|
end
|
|
113
|
-
dedup_source_list(source_list)
|
|
129
|
+
dedup_source_list(source_list)
|
|
114
130
|
end
|
|
115
131
|
end
|
|
116
132
|
|
|
@@ -120,8 +136,12 @@ module SecureHeaders
|
|
|
120
136
|
end
|
|
121
137
|
|
|
122
138
|
# Discard any 'none' values if more directives are supplied since none may override values.
|
|
123
|
-
def reject_all_values_if_none
|
|
124
|
-
|
|
139
|
+
def reject_all_values_if_none(source_list)
|
|
140
|
+
if source_list.length > 1
|
|
141
|
+
source_list.reject { |value| value == NONE }
|
|
142
|
+
else
|
|
143
|
+
source_list
|
|
144
|
+
end
|
|
125
145
|
end
|
|
126
146
|
|
|
127
147
|
# Removes duplicates and sources that already match an existing wild card.
|
|
@@ -143,12 +163,14 @@ module SecureHeaders
|
|
|
143
163
|
|
|
144
164
|
# Private: append a nonce to the script/style directories if script_nonce
|
|
145
165
|
# or style_nonce are provided.
|
|
146
|
-
def populate_nonces
|
|
166
|
+
def populate_nonces(directive, source_list)
|
|
147
167
|
case directive
|
|
148
168
|
when SCRIPT_SRC
|
|
149
169
|
append_nonce(source_list, @script_nonce)
|
|
150
170
|
when STYLE_SRC
|
|
151
171
|
append_nonce(source_list, @style_nonce)
|
|
172
|
+
else
|
|
173
|
+
source_list
|
|
152
174
|
end
|
|
153
175
|
end
|
|
154
176
|
|
|
@@ -165,19 +187,23 @@ module SecureHeaders
|
|
|
165
187
|
source_list << UNSAFE_INLINE
|
|
166
188
|
end
|
|
167
189
|
end
|
|
190
|
+
|
|
191
|
+
source_list
|
|
168
192
|
end
|
|
169
193
|
|
|
170
194
|
# Private: return the list of directives that are supported by the user agent,
|
|
171
195
|
# starting with default-src and ending with report-uri.
|
|
172
196
|
def directives
|
|
173
|
-
[
|
|
197
|
+
[
|
|
198
|
+
DEFAULT_SRC,
|
|
174
199
|
BODY_DIRECTIVES.select { |key| supported_directives.include?(key) },
|
|
175
|
-
REPORT_URI
|
|
200
|
+
REPORT_URI
|
|
201
|
+
].flatten
|
|
176
202
|
end
|
|
177
203
|
|
|
178
204
|
# Private: Remove scheme from source expressions.
|
|
179
|
-
def strip_source_schemes
|
|
180
|
-
source_list.map
|
|
205
|
+
def strip_source_schemes(source_list)
|
|
206
|
+
source_list.map { |source_expression| source_expression.sub(HTTP_SCHEME_REGEX, "") }
|
|
181
207
|
end
|
|
182
208
|
|
|
183
209
|
# Private: determine which directives are supported for the given user agent.
|
|
@@ -198,7 +224,7 @@ module SecureHeaders
|
|
|
198
224
|
end
|
|
199
225
|
|
|
200
226
|
def nonces_supported?
|
|
201
|
-
@nonces_supported ||=
|
|
227
|
+
@nonces_supported ||= self.class.nonces_supported?(@parsed_ua)
|
|
202
228
|
end
|
|
203
229
|
|
|
204
230
|
def symbol_to_hyphen_case(sym)
|