secure_headers 3.4.1 → 3.5.0.pre
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of secure_headers might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +32 -3
- data/lib/secure_headers.rb +122 -56
- data/lib/secure_headers/configuration.rb +56 -35
- data/lib/secure_headers/headers/content_security_policy.rb +60 -35
- data/lib/secure_headers/headers/content_security_policy_config.rb +128 -0
- data/lib/secure_headers/headers/policy_management.rb +13 -21
- 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 +1 -1
- data/spec/lib/secure_headers/view_helpers_spec.rb +9 -6
- data/spec/lib/secure_headers_spec.rb +236 -58
- data/spec/spec_helper.rb +1 -1
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a34fab69fada104ff674198e9ee40271bcc86aea
|
4
|
+
data.tar.gz: c95c8b38f37b47dd66cf202c83fd4923b1aa3df9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 93d7bc9412154b01231352eb801f9a6a72438d6ce442dc70e8f11a7d013a466ab3fcadfdea6abda7642361e6baa509c1a4087da0630f5263d26c42b5c384a890
|
7
|
+
data.tar.gz: 403ac9a435812bfbbe7ccd4edeb96d646975ab769582680d19c58c48145609cb06ff1095a683e4f6853d74dd4688a7fb99f35f56147a583af6835b2126c957c3
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -36,7 +36,7 @@ SecureHeaders::Configuration.default do |config|
|
|
36
36
|
secure: true, # mark all cookies as "Secure"
|
37
37
|
httponly: true, # mark all cookies as "HttpOnly"
|
38
38
|
samesite: {
|
39
|
-
|
39
|
+
lax: true # mark all cookies as SameSite=lax
|
40
40
|
}
|
41
41
|
}
|
42
42
|
config.hsts = "max-age=#{20.years.to_i}; includeSubdomains; preload"
|
@@ -48,7 +48,7 @@ SecureHeaders::Configuration.default do |config|
|
|
48
48
|
config.referrer_policy = "origin-when-cross-origin"
|
49
49
|
config.csp = {
|
50
50
|
# "meta" values. these will shaped the header, but the values are not included in the header.
|
51
|
-
report_only: true, # default: false
|
51
|
+
report_only: true, # default: false [DEPRECATED: instead, configure csp_report_only]
|
52
52
|
preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content.
|
53
53
|
|
54
54
|
# directive values: these values will directly translate into source directives
|
@@ -69,6 +69,10 @@ SecureHeaders::Configuration.default do |config|
|
|
69
69
|
upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/
|
70
70
|
report_uri: %w(https://report-uri.io/example-csp)
|
71
71
|
}
|
72
|
+
config.csp_report_only = config.csp.merge({
|
73
|
+
img_src: %w(somewhereelse.com),
|
74
|
+
report_uri: %w(https://report-uri.io/example-csp-report-only)
|
75
|
+
})
|
72
76
|
config.hpkp = {
|
73
77
|
report_only: false,
|
74
78
|
max_age: 60.days.to_i,
|
@@ -92,7 +96,32 @@ use SecureHeaders::Middleware
|
|
92
96
|
|
93
97
|
## Default values
|
94
98
|
|
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).
|
99
|
+
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:
|
100
|
+
|
101
|
+
```
|
102
|
+
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'
|
103
|
+
Strict-Transport-Security: max-age=631138519
|
104
|
+
X-Content-Type-Options: nosniff
|
105
|
+
X-Download-Options: noopen
|
106
|
+
X-Frame-Options: sameorigin
|
107
|
+
X-Permitted-Cross-Domain-Policies: none
|
108
|
+
X-Xss-Protection: 1; mode=block
|
109
|
+
```
|
110
|
+
|
111
|
+
### Default CSP
|
112
|
+
|
113
|
+
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.
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
Configuration.default do |config|
|
117
|
+
config.csp = SecureHeaders::OPT_OUT # If this line is omitted, we will assume you meant to opt out.
|
118
|
+
config.csp_report_only = {
|
119
|
+
default_src: %w('self')
|
120
|
+
}
|
121
|
+
end
|
122
|
+
```
|
123
|
+
|
124
|
+
If **
|
96
125
|
|
97
126
|
## Named Appends
|
98
127
|
|
data/lib/secure_headers.rb
CHANGED
@@ -14,18 +14,43 @@ require "secure_headers/middleware"
|
|
14
14
|
require "secure_headers/railtie"
|
15
15
|
require "secure_headers/view_helper"
|
16
16
|
require "useragent"
|
17
|
+
require "singleton"
|
17
18
|
|
18
19
|
# All headers (except for hpkp) have a default value. Provide SecureHeaders::OPT_OUT
|
19
20
|
# or ":optout_of_protection" as a config value to disable a given header
|
20
21
|
module SecureHeaders
|
21
|
-
|
22
|
+
class NoOpHeaderConfig
|
23
|
+
include Singleton
|
24
|
+
|
25
|
+
def boom(arg = nil)
|
26
|
+
raise "Illegal State: attempted to modify NoOpHeaderConfig. Create a new config instead."
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_h
|
30
|
+
{}
|
31
|
+
end
|
32
|
+
|
33
|
+
def dup
|
34
|
+
self.class.instance
|
35
|
+
end
|
36
|
+
|
37
|
+
def opt_out?
|
38
|
+
true
|
39
|
+
end
|
40
|
+
|
41
|
+
alias_method :[], :boom
|
42
|
+
alias_method :[]=, :boom
|
43
|
+
alias_method :keys, :boom
|
44
|
+
end
|
45
|
+
|
46
|
+
OPT_OUT = NoOpHeaderConfig.instance
|
22
47
|
SECURE_HEADERS_CONFIG = "secure_headers_request_config".freeze
|
23
48
|
NONCE_KEY = "secure_headers_content_security_policy_nonce".freeze
|
24
49
|
HTTPS = "https".freeze
|
25
|
-
CSP = ContentSecurityPolicy
|
26
50
|
|
27
51
|
ALL_HEADER_CLASSES = [
|
28
|
-
|
52
|
+
ContentSecurityPolicyConfig,
|
53
|
+
ContentSecurityPolicyReportOnlyConfig,
|
29
54
|
StrictTransportSecurity,
|
30
55
|
PublicKeyPins,
|
31
56
|
ReferrerPolicy,
|
@@ -36,7 +61,10 @@ module SecureHeaders
|
|
36
61
|
XXssProtection
|
37
62
|
].freeze
|
38
63
|
|
39
|
-
ALL_HEADERS_BESIDES_CSP = (
|
64
|
+
ALL_HEADERS_BESIDES_CSP = (
|
65
|
+
ALL_HEADER_CLASSES -
|
66
|
+
[ContentSecurityPolicyConfig, ContentSecurityPolicyReportOnlyConfig]
|
67
|
+
).freeze
|
40
68
|
|
41
69
|
# Headers set on http requests (excludes STS and HPKP)
|
42
70
|
HTTP_HEADER_CLASSES =
|
@@ -50,13 +78,25 @@ module SecureHeaders
|
|
50
78
|
#
|
51
79
|
# additions - a hash containing directives. e.g.
|
52
80
|
# script_src: %w(another-host.com)
|
53
|
-
def override_content_security_policy_directives(request, additions)
|
54
|
-
config =
|
55
|
-
|
56
|
-
|
81
|
+
def override_content_security_policy_directives(request, additions, target = nil)
|
82
|
+
config, target = config_and_target(request, target)
|
83
|
+
|
84
|
+
if [:both, :enforced].include?(target)
|
85
|
+
if config.csp.opt_out?
|
86
|
+
config.csp = ContentSecurityPolicyConfig.new({})
|
87
|
+
end
|
88
|
+
|
89
|
+
config.csp.merge!(additions)
|
90
|
+
end
|
91
|
+
|
92
|
+
if [:both, :report_only].include?(target)
|
93
|
+
if config.csp_report_only.opt_out?
|
94
|
+
config.csp_report_only = ContentSecurityPolicyReportOnlyConfig.new({})
|
95
|
+
end
|
96
|
+
|
97
|
+
config.csp_report_only.merge!(additions)
|
57
98
|
end
|
58
99
|
|
59
|
-
config.dynamic_csp = config.current_csp.merge(additions)
|
60
100
|
override_secure_headers_request_config(request, config)
|
61
101
|
end
|
62
102
|
|
@@ -66,9 +106,17 @@ module SecureHeaders
|
|
66
106
|
#
|
67
107
|
# additions - a hash containing directives. e.g.
|
68
108
|
# script_src: %w(another-host.com)
|
69
|
-
def append_content_security_policy_directives(request, additions)
|
70
|
-
config =
|
71
|
-
|
109
|
+
def append_content_security_policy_directives(request, additions, target = nil)
|
110
|
+
config, target = config_and_target(request, target)
|
111
|
+
|
112
|
+
if [:both, :enforced].include?(target) && !config.csp.opt_out?
|
113
|
+
config.csp.append(additions)
|
114
|
+
end
|
115
|
+
|
116
|
+
if [:both, :report_only].include?(target) && !config.csp_report_only.opt_out?
|
117
|
+
config.csp_report_only.append(additions)
|
118
|
+
end
|
119
|
+
|
72
120
|
override_secure_headers_request_config(request, config)
|
73
121
|
end
|
74
122
|
|
@@ -112,12 +160,28 @@ module SecureHeaders
|
|
112
160
|
# returned is meant to be merged into the header value from `@app.call(env)`
|
113
161
|
# in Rack middleware.
|
114
162
|
def header_hash_for(request)
|
115
|
-
config = config_for(request)
|
116
|
-
|
117
|
-
|
163
|
+
config = config_for(request, prevent_dup = true)
|
164
|
+
headers = config.cached_headers
|
165
|
+
user_agent = UserAgent.parse(request.user_agent)
|
166
|
+
|
167
|
+
if !config.csp.opt_out? && config.csp.modified?
|
168
|
+
headers = update_cached_csp(config.csp, headers, user_agent)
|
118
169
|
end
|
119
170
|
|
120
|
-
|
171
|
+
if !config.csp_report_only.opt_out? && config.csp_report_only.modified?
|
172
|
+
headers = update_cached_csp(config.csp_report_only, headers, user_agent)
|
173
|
+
end
|
174
|
+
|
175
|
+
header_classes_for(request).each_with_object({}) do |klass, hash|
|
176
|
+
if header = headers[klass::CONFIG_KEY]
|
177
|
+
header_name, value = if [ContentSecurityPolicyConfig, ContentSecurityPolicyReportOnlyConfig].include?(klass)
|
178
|
+
csp_header_for_ua(header, user_agent)
|
179
|
+
else
|
180
|
+
header
|
181
|
+
end
|
182
|
+
hash[header_name] = value
|
183
|
+
end
|
184
|
+
end
|
121
185
|
end
|
122
186
|
|
123
187
|
# Public: specify which named override will be used for this request.
|
@@ -138,7 +202,7 @@ module SecureHeaders
|
|
138
202
|
#
|
139
203
|
# Returns the nonce
|
140
204
|
def content_security_policy_script_nonce(request)
|
141
|
-
content_security_policy_nonce(request,
|
205
|
+
content_security_policy_nonce(request, ContentSecurityPolicy::SCRIPT_SRC)
|
142
206
|
end
|
143
207
|
|
144
208
|
# Public: gets or creates a nonce for CSP.
|
@@ -147,7 +211,7 @@ module SecureHeaders
|
|
147
211
|
#
|
148
212
|
# Returns the nonce
|
149
213
|
def content_security_policy_style_nonce(request)
|
150
|
-
content_security_policy_nonce(request,
|
214
|
+
content_security_policy_nonce(request, ContentSecurityPolicy::STYLE_SRC)
|
151
215
|
end
|
152
216
|
|
153
217
|
# Public: Retreives the config for a given header type:
|
@@ -155,11 +219,15 @@ module SecureHeaders
|
|
155
219
|
# Checks to see if there is an override for this request, then
|
156
220
|
# Checks to see if a named override is used for this request, then
|
157
221
|
# Falls back to the global config
|
158
|
-
def config_for(request)
|
222
|
+
def config_for(request, prevent_dup = false)
|
159
223
|
config = request.env[SECURE_HEADERS_CONFIG] ||
|
160
224
|
Configuration.get(Configuration::DEFAULT_CONFIG)
|
161
225
|
|
162
|
-
|
226
|
+
|
227
|
+
# Global configs are frozen, per-request configs are not. When we're not
|
228
|
+
# making modifications to the config, prevent_dup ensures we don't dup
|
229
|
+
# the object unnecessarily. It's not necessarily frozen to begin with.
|
230
|
+
if config.frozen? && !prevent_dup
|
163
231
|
config.dup
|
164
232
|
else
|
165
233
|
config
|
@@ -167,13 +235,38 @@ module SecureHeaders
|
|
167
235
|
end
|
168
236
|
|
169
237
|
private
|
238
|
+
TARGETS = [:both, :enforced, :report_only]
|
239
|
+
def raise_on_unknown_target(target)
|
240
|
+
unless TARGETS.include?(target)
|
241
|
+
raise "Unrecognized target: #{target}. Must be [:both, :enforced, :report_only]"
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
def config_and_target(request, target)
|
246
|
+
config = config_for(request)
|
247
|
+
target = guess_target(config) unless target
|
248
|
+
raise_on_unknown_target(target)
|
249
|
+
[config, target]
|
250
|
+
end
|
251
|
+
|
252
|
+
def guess_target(config)
|
253
|
+
if !config.csp.opt_out? && !config.csp_report_only.opt_out?
|
254
|
+
:both
|
255
|
+
elsif !config.csp.opt_out?
|
256
|
+
:enforced
|
257
|
+
elsif !config.csp_report_only.opt_out?
|
258
|
+
:report_only
|
259
|
+
else
|
260
|
+
:both
|
261
|
+
end
|
262
|
+
end
|
170
263
|
|
171
264
|
# Private: gets or creates a nonce for CSP.
|
172
265
|
#
|
173
266
|
# Returns the nonce
|
174
267
|
def content_security_policy_nonce(request, script_or_style)
|
175
268
|
request.env[NONCE_KEY] ||= SecureRandom.base64(32).chomp
|
176
|
-
nonce_key = script_or_style ==
|
269
|
+
nonce_key = script_or_style == ContentSecurityPolicy::SCRIPT_SRC ? :script_nonce : :style_nonce
|
177
270
|
append_content_security_policy_directives(request, nonce_key => request.env[NONCE_KEY])
|
178
271
|
request.env[NONCE_KEY]
|
179
272
|
end
|
@@ -198,21 +291,12 @@ module SecureHeaders
|
|
198
291
|
end
|
199
292
|
end
|
200
293
|
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
if header = headers[klass::CONFIG_KEY]
|
208
|
-
header_name, value = if klass == CSP
|
209
|
-
csp_header_for_ua(header, request)
|
210
|
-
else
|
211
|
-
header
|
212
|
-
end
|
213
|
-
hash[header_name] = value
|
214
|
-
end
|
215
|
-
end
|
294
|
+
def update_cached_csp(config, headers, user_agent)
|
295
|
+
headers = Configuration.send(:deep_copy, headers)
|
296
|
+
headers[config.class::CONFIG_KEY] = {}
|
297
|
+
variation = ContentSecurityPolicy.ua_to_variation(user_agent)
|
298
|
+
headers[config.class::CONFIG_KEY][variation] = ContentSecurityPolicy.make_header(config, user_agent)
|
299
|
+
headers
|
216
300
|
end
|
217
301
|
|
218
302
|
# Private: chooses the applicable CSP header for the provided user agent.
|
@@ -220,26 +304,8 @@ module SecureHeaders
|
|
220
304
|
# headers - a hash of header_config_key => [header_name, header_value]
|
221
305
|
#
|
222
306
|
# Returns a CSP [header, value] array
|
223
|
-
def csp_header_for_ua(headers,
|
224
|
-
headers[
|
225
|
-
end
|
226
|
-
|
227
|
-
# Private: optionally build a header with a given configure
|
228
|
-
#
|
229
|
-
# klass - corresponding Class for a given header
|
230
|
-
# config - A string, symbol, or hash config for the header
|
231
|
-
# user_agent - A string representing the UA (only used for CSP feature sniffing)
|
232
|
-
#
|
233
|
-
# Returns a 2 element array [header_name, header_value] or nil if config
|
234
|
-
# is OPT_OUT
|
235
|
-
def make_header(klass, header_config, user_agent = nil)
|
236
|
-
unless header_config == OPT_OUT
|
237
|
-
if klass == CSP
|
238
|
-
klass.make_header(header_config, user_agent)
|
239
|
-
else
|
240
|
-
klass.make_header(header_config)
|
241
|
-
end
|
242
|
-
end
|
307
|
+
def csp_header_for_ua(headers, user_agent)
|
308
|
+
headers[ContentSecurityPolicy.ua_to_variation(user_agent)]
|
243
309
|
end
|
244
310
|
end
|
245
311
|
|
@@ -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
|
|
@@ -142,8 +141,8 @@ module SecureHeaders
|
|
142
141
|
def dup
|
143
142
|
copy = self.class.new
|
144
143
|
copy.cookies = @cookies
|
145
|
-
copy.csp =
|
146
|
-
copy.
|
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
|