Skip to content

Commit 532c30e

Browse files
authored
Merge pull request #382 from twitter/nonce-sniffing
unconditionally send nonces and unsafe-inline when working with nonces
2 parents be13243 + 6956833 commit 532c30e

File tree

5 files changed

+20
-61
lines changed

5 files changed

+20
-61
lines changed

docs/upgrading-to-6-0.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,9 @@ Prior to 6.0.0 SecureHeaders pre-built and cached the headers that corresponded
4242
## Configuration the default configuration more than once will result in an Exception
4343

4444
Prior to 6.0.0 you could conceivably, though unlikely, have `Configure#default` called more than once. Because configurations are dynamic, configuring more than once could result in unexpected behavior. So, as of 6.0.0 we raise `AlreadyConfiguredError` if the default configuration is setup more than once.
45+
46+
## Nonce behavior and console warnings
47+
48+
Since the first commit, reducing browser console messages was a goal. It led to overly complicated and error-prone UA sniffing. Nowadays, consoles warn on completely legitimate use of features meant to be backwards compatible. So the goal is impossible and the impact is negative, so eliminating code using sniffing is a goal.
49+
50+
The first example: we will now send `'unsafe-inline'` along with nonce source expressions. This will generate warnings in some consoles but is 100% valid use and was a design goal of CSP in the early days. The concept of versioning CSP lost out and so we're left with backward compatibility as our only option.

lib/secure_headers/headers/content_security_policy.rb

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -213,11 +213,7 @@ def populate_nonces(directive, source_list)
213213
# unsafe-inline, this is more concise.
214214
def append_nonce(source_list, nonce)
215215
if nonce
216-
if nonces_supported?
217-
source_list << "'nonce-#{nonce}'"
218-
else
219-
source_list << UNSAFE_INLINE
220-
end
216+
source_list.push("'nonce-#{nonce}'", UNSAFE_INLINE)
221217
end
222218

223219
source_list
@@ -257,10 +253,6 @@ def supported_directives
257253
end
258254
end
259255

260-
def nonces_supported?
261-
@nonces_supported ||= self.class.nonces_supported?(@parsed_ua)
262-
end
263-
264256
def symbol_to_hyphen_case(sym)
265257
sym.to_s.tr("_", "-")
266258
end

lib/secure_headers/headers/policy_management.rb

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ def self.included(base)
55
base.extend(ClassMethods)
66
end
77

8-
MODERN_BROWSERS = %w(Chrome Opera Firefox)
98
DEFAULT_CONFIG = {
109
default_src: %w(https:),
1110
img_src: %w(https: data: 'self'),
@@ -238,15 +237,6 @@ def validate_config!(config)
238237
end
239238
end
240239

241-
# Public: check if a user agent supports CSP nonces
242-
#
243-
# user_agent - a String or a UserAgent object
244-
def nonces_supported?(user_agent)
245-
user_agent = UserAgent.parse(user_agent) if user_agent.is_a?(String)
246-
MODERN_BROWSERS.include?(user_agent.browser) ||
247-
user_agent.browser == "Safari" && (user_agent.version || CSP::FALLBACK_VERSION) >= CSP::VERSION_10
248-
end
249-
250240
# Public: combine the values from two different configs.
251241
#
252242
# original - the main config

spec/lib/secure_headers/headers/content_security_policy_spec.rb

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ module SecureHeaders
124124

125125
it "supports strict-dynamic" do
126126
csp = ContentSecurityPolicy.new({default_src: %w('self'), script_src: [ContentSecurityPolicy::STRICT_DYNAMIC], script_nonce: 123456}, USER_AGENTS[:chrome])
127-
expect(csp.value).to eq("default-src 'self'; script-src 'strict-dynamic' 'nonce-123456'")
127+
expect(csp.value).to eq("default-src 'self'; script-src 'strict-dynamic' 'nonce-123456' 'unsafe-inline'")
128128
end
129129

130130
context "browser sniffing" do
@@ -143,44 +143,44 @@ module SecureHeaders
143143

144144
it "does not filter any directives for Chrome" do
145145
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:chrome])
146-
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; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; worker-src worker-src.com; report-uri report-uri.com")
146+
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; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; worker-src worker-src.com; report-uri report-uri.com")
147147
end
148148

149149
it "does not filter any directives for Opera" do
150150
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:opera])
151-
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; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; worker-src worker-src.com; report-uri report-uri.com")
151+
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; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; worker-src worker-src.com; report-uri report-uri.com")
152152
end
153153

154154
it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do
155155
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox])
156-
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; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
156+
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; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
157157
end
158158

159159
it "filters blocked-all-mixed-content, frame-src, and plugin-types for firefox 46 and higher" do
160160
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox46])
161-
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; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
161+
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; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
162162
end
163163

164-
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
164+
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, hash sources, and plugin-types for Edge" do
165165
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:edge])
166-
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 allow-forms; script-src script-src.com 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
166+
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 allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
167167
end
168168

169-
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
169+
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, hash sources, and plugin-types for safari" do
170170
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari6])
171-
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 allow-forms; script-src script-src.com 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
171+
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 allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
172172
end
173173

174-
it "adds 'unsafe-inline', filters blocked-all-mixed-content, upgrade-insecure-requests, nonce sources, and hash sources for safari 10 and higher" do
174+
it "adds 'unsafe-inline', filters blocked-all-mixed-content, upgrade-insecure-requests, and hash sources for safari 10 and higher" do
175175
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari10])
176-
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; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; report-uri report-uri.com")
176+
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; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
177177
end
178178

179179
it "falls back to standard Firefox defaults when the useragent version is not present" do
180180
ua = USER_AGENTS[:firefox].dup
181181
allow(ua).to receive(:version).and_return(nil)
182182
policy = ContentSecurityPolicy.new(complex_opts, ua)
183-
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; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
183+
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; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
184184
end
185185
end
186186
end

spec/lib/secure_headers_spec.rb

Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -287,21 +287,6 @@ module SecureHeaders
287287
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src https:; img-src data:; script-src 'self'")
288288
end
289289

290-
it "does not append a nonce when the browser does not support it" do
291-
Configuration.default do |config|
292-
config.csp = {
293-
default_src: %w('self'),
294-
script_src: %w(mycdn.com 'unsafe-inline'),
295-
style_src: %w('self')
296-
}
297-
end
298-
299-
safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari5]))
300-
SecureHeaders.content_security_policy_script_nonce(safari_request)
301-
hash = SecureHeaders.header_hash_for(safari_request)
302-
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline'; style-src 'self'")
303-
end
304-
305290
it "appends a nonce to the script-src when used" do
306291
Configuration.default do |config|
307292
config.csp = {
@@ -319,21 +304,7 @@ module SecureHeaders
319304
SecureHeaders.content_security_policy_script_nonce(chrome_request)
320305

321306
hash = SecureHeaders.header_hash_for(chrome_request)
322-
expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'; style-src 'self'")
323-
end
324-
325-
it "uses a nonce for safari 10+" do
326-
Configuration.default do |config|
327-
config.csp = {
328-
default_src: %w('self'),
329-
script_src: %w(mycdn.com)
330-
}
331-
end
332-
333-
safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari10]))
334-
nonce = SecureHeaders.content_security_policy_script_nonce(safari_request)
335-
hash = SecureHeaders.header_hash_for(safari_request)
336-
expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'")
307+
expect(hash["Content-Security-Policy"]).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}' 'unsafe-inline'; style-src 'self'")
337308
end
338309

339310
it "does not support the deprecated `report_only: true` format" do

0 commit comments

Comments
 (0)