secure_headers 3.4.1 → 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 +5 -0
- data/Gemfile +1 -0
- data/README.md +31 -7
- data/lib/secure_headers/configuration.rb +57 -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 +19 -18
- data/lib/secure_headers/view_helper.rb +8 -0
- data/lib/secure_headers.rb +122 -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 +250 -58
- 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
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -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,30 @@ 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
|
+
```
|
|
96
120
|
|
|
97
121
|
## Named Appends
|
|
98
122
|
|
|
@@ -85,7 +85,6 @@ module SecureHeaders
|
|
|
85
85
|
ALL_HEADER_CLASSES.each do |klass|
|
|
86
86
|
config.send("#{klass::CONFIG_KEY}=", OPT_OUT)
|
|
87
87
|
end
|
|
88
|
-
config.dynamic_csp = OPT_OUT
|
|
89
88
|
end
|
|
90
89
|
|
|
91
90
|
add_configuration(NOOP_CONFIGURATION, noop_config)
|
|
@@ -94,6 +93,7 @@ module SecureHeaders
|
|
|
94
93
|
# Public: perform a basic deep dup. The shallow copy provided by dup/clone
|
|
95
94
|
# can lead to modifying parent objects.
|
|
96
95
|
def deep_copy(config)
|
|
96
|
+
return unless config
|
|
97
97
|
config.each_with_object({}) do |(key, value), hash|
|
|
98
98
|
hash[key] = if value.is_a?(Array)
|
|
99
99
|
value.dup
|
|
@@ -114,13 +114,11 @@ module SecureHeaders
|
|
|
114
114
|
end
|
|
115
115
|
end
|
|
116
116
|
|
|
117
|
-
attr_accessor :dynamic_csp
|
|
118
|
-
|
|
119
117
|
attr_writer :hsts, :x_frame_options, :x_content_type_options,
|
|
120
118
|
:x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies,
|
|
121
119
|
:referrer_policy
|
|
122
120
|
|
|
123
|
-
attr_reader :cached_headers, :csp, :cookies, :hpkp, :hpkp_report_host
|
|
121
|
+
attr_reader :cached_headers, :csp, :cookies, :csp_report_only, :hpkp, :hpkp_report_host
|
|
124
122
|
|
|
125
123
|
HASH_CONFIG_FILE = ENV["secure_headers_generated_hashes_file"] || "config/secure_headers_generated_hashes.yml"
|
|
126
124
|
if File.exists?(HASH_CONFIG_FILE)
|
|
@@ -132,7 +130,8 @@ module SecureHeaders
|
|
|
132
130
|
def initialize(&block)
|
|
133
131
|
self.hpkp = OPT_OUT
|
|
134
132
|
self.referrer_policy = OPT_OUT
|
|
135
|
-
self.csp =
|
|
133
|
+
self.csp = ContentSecurityPolicyConfig.new(ContentSecurityPolicyConfig::DEFAULT)
|
|
134
|
+
self.csp_report_only = OPT_OUT
|
|
136
135
|
instance_eval &block if block_given?
|
|
137
136
|
end
|
|
138
137
|
|
|
@@ -141,9 +140,9 @@ module SecureHeaders
|
|
|
141
140
|
# Returns a deep-dup'd copy of this configuration.
|
|
142
141
|
def dup
|
|
143
142
|
copy = self.class.new
|
|
144
|
-
copy.cookies = @cookies
|
|
145
|
-
copy.csp =
|
|
146
|
-
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
|
|
147
146
|
copy.cached_headers = self.class.send(:deep_copy_if_hash, @cached_headers)
|
|
148
147
|
copy.x_content_type_options = @x_content_type_options
|
|
149
148
|
copy.hsts = @hsts
|
|
@@ -159,9 +158,6 @@ module SecureHeaders
|
|
|
159
158
|
|
|
160
159
|
def opt_out(header)
|
|
161
160
|
send("#{header}=", OPT_OUT)
|
|
162
|
-
if header == CSP::CONFIG_KEY
|
|
163
|
-
dynamic_csp = OPT_OUT
|
|
164
|
-
end
|
|
165
161
|
self.cached_headers.delete(header)
|
|
166
162
|
end
|
|
167
163
|
|
|
@@ -170,20 +166,6 @@ module SecureHeaders
|
|
|
170
166
|
self.cached_headers[XFrameOptions::CONFIG_KEY] = XFrameOptions.make_header(value)
|
|
171
167
|
end
|
|
172
168
|
|
|
173
|
-
# Public: generated cached headers for a specific user agent.
|
|
174
|
-
def rebuild_csp_header_cache!(user_agent)
|
|
175
|
-
self.cached_headers[CSP::CONFIG_KEY] = {}
|
|
176
|
-
unless current_csp == OPT_OUT
|
|
177
|
-
user_agent = UserAgent.parse(user_agent)
|
|
178
|
-
variation = CSP.ua_to_variation(user_agent)
|
|
179
|
-
self.cached_headers[CSP::CONFIG_KEY][variation] = CSP.make_header(current_csp, user_agent)
|
|
180
|
-
end
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
def current_csp
|
|
184
|
-
@dynamic_csp || @csp
|
|
185
|
-
end
|
|
186
|
-
|
|
187
169
|
# Public: validates all configurations values.
|
|
188
170
|
#
|
|
189
171
|
# Raises various configuration errors if any invalid config is detected.
|
|
@@ -192,6 +174,7 @@ module SecureHeaders
|
|
|
192
174
|
def validate_config!
|
|
193
175
|
StrictTransportSecurity.validate_config!(@hsts)
|
|
194
176
|
ContentSecurityPolicy.validate_config!(@csp)
|
|
177
|
+
ContentSecurityPolicy.validate_config!(@csp_report_only)
|
|
195
178
|
ReferrerPolicy.validate_config!(@referrer_policy)
|
|
196
179
|
XFrameOptions.validate_config!(@x_frame_options)
|
|
197
180
|
XContentTypeOptions.validate_config!(@x_content_type_options)
|
|
@@ -207,16 +190,50 @@ module SecureHeaders
|
|
|
207
190
|
@cookies = (@cookies || {}).merge(secure: secure_cookies)
|
|
208
191
|
end
|
|
209
192
|
|
|
210
|
-
protected
|
|
211
|
-
|
|
212
193
|
def csp=(new_csp)
|
|
213
|
-
if
|
|
214
|
-
|
|
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
|
|
215
227
|
end
|
|
216
228
|
|
|
217
|
-
@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
|
|
218
233
|
end
|
|
219
234
|
|
|
235
|
+
protected
|
|
236
|
+
|
|
220
237
|
def cookies=(cookies)
|
|
221
238
|
@cookies = cookies
|
|
222
239
|
end
|
|
@@ -269,12 +286,16 @@ module SecureHeaders
|
|
|
269
286
|
#
|
|
270
287
|
# Returns nothing
|
|
271
288
|
def generate_csp_headers(headers)
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
|
278
299
|
end
|
|
279
300
|
end
|
|
280
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)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
module SecureHeaders
|
|
2
|
+
module DynamicConfig
|
|
3
|
+
def self.included(base)
|
|
4
|
+
base.send(:attr_writer, :modified)
|
|
5
|
+
base.send(:attr_reader, *base.attrs)
|
|
6
|
+
base.attrs.each do |attr|
|
|
7
|
+
base.send(:define_method, "#{attr}=") do |value|
|
|
8
|
+
if self.class.attrs.include?(attr)
|
|
9
|
+
value = value.dup if PolicyManagement::DIRECTIVE_VALUE_TYPES[attr] == :source_list
|
|
10
|
+
prev_value = self.instance_variable_get("@#{attr}")
|
|
11
|
+
self.instance_variable_set("@#{attr}", value)
|
|
12
|
+
if prev_value != self.instance_variable_get("@#{attr}")
|
|
13
|
+
@modified = true
|
|
14
|
+
end
|
|
15
|
+
else
|
|
16
|
+
raise ContentSecurityPolicyConfigError, "Unknown config directive: #{attr}=#{value}"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(hash)
|
|
23
|
+
from_hash(hash)
|
|
24
|
+
@modified = false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def update_directive(directive, value)
|
|
28
|
+
self.send("#{directive}=", value)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def directive_value(directive)
|
|
32
|
+
if self.class.attrs.include?(directive)
|
|
33
|
+
self.send(directive)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def modified?
|
|
38
|
+
@modified
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def merge(new_hash)
|
|
42
|
+
ContentSecurityPolicy.combine_policies(self.to_h, new_hash)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def merge!(new_hash)
|
|
46
|
+
from_hash(new_hash)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def append(new_hash)
|
|
50
|
+
from_hash(ContentSecurityPolicy.combine_policies(self.to_h, new_hash))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def to_h
|
|
54
|
+
self.class.attrs.each_with_object({}) do |key, hash|
|
|
55
|
+
hash[key] = self.send(key)
|
|
56
|
+
end.reject { |_, v| v.nil? }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def dup
|
|
60
|
+
self.class.new(self.to_h)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def opt_out?
|
|
64
|
+
false
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def ==(o)
|
|
68
|
+
self.class == o.class && self.to_h == o.to_h
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
alias_method :[], :directive_value
|
|
72
|
+
alias_method :[]=, :update_directive
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
def from_hash(hash)
|
|
76
|
+
hash.keys.reject { |k| hash[k].nil? }.map do |k|
|
|
77
|
+
if self.class.attrs.include?(k)
|
|
78
|
+
self.send("#{k}=", hash[k])
|
|
79
|
+
else
|
|
80
|
+
raise ContentSecurityPolicyConfigError, "Unknown config directive: #{k}=#{hash[k]}"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
class ContentSecurityPolicyConfigError < StandardError; end
|
|
87
|
+
class ContentSecurityPolicyConfig
|
|
88
|
+
CONFIG_KEY = :csp
|
|
89
|
+
HEADER_NAME = "Content-Security-Policy".freeze
|
|
90
|
+
|
|
91
|
+
def self.attrs
|
|
92
|
+
PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
include DynamicConfig
|
|
96
|
+
|
|
97
|
+
# based on what was suggested in https://github.com/rails/rails/pull/24961/files
|
|
98
|
+
DEFAULT = {
|
|
99
|
+
default_src: %w('self' https:),
|
|
100
|
+
font_src: %w('self' https: data:),
|
|
101
|
+
img_src: %w('self' https: data:),
|
|
102
|
+
object_src: %w('none'),
|
|
103
|
+
script_src: %w(https:),
|
|
104
|
+
style_src: %w('self' https: 'unsafe-inline')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
def report_only?
|
|
108
|
+
false
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def make_report_only
|
|
112
|
+
ContentSecurityPolicyReportOnlyConfig.new(self.to_h)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
class ContentSecurityPolicyReportOnlyConfig < ContentSecurityPolicyConfig
|
|
117
|
+
CONFIG_KEY = :csp_report_only
|
|
118
|
+
HEADER_NAME = "Content-Security-Policy-Report-Only".freeze
|
|
119
|
+
|
|
120
|
+
def report_only?
|
|
121
|
+
true
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def make_report_only
|
|
125
|
+
self
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|