Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(182)

Side by Side Diff: extensions/common/csp_validator.cc

Issue 2563843002: Restrict app sandbox's CSP to disallow loading web content in them. (Closed)
Patch Set: sync Created 4 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright 2013 The Chromium Authors. All rights reserved. 1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include "extensions/common/csp_validator.h" 5 #include "extensions/common/csp_validator.h"
6 6
7 #include <stddef.h> 7 #include <stddef.h>
8 8
9 #include <vector> 9 #include <vector>
10 10
(...skipping 10 matching lines...) Expand all
21 21
22 namespace extensions { 22 namespace extensions {
23 23
24 namespace csp_validator { 24 namespace csp_validator {
25 25
26 namespace { 26 namespace {
27 27
28 const char kDefaultSrc[] = "default-src"; 28 const char kDefaultSrc[] = "default-src";
29 const char kScriptSrc[] = "script-src"; 29 const char kScriptSrc[] = "script-src";
30 const char kObjectSrc[] = "object-src"; 30 const char kObjectSrc[] = "object-src";
31 const char kFrameSrc[] = "frame-src";
32 const char kChildSrc[] = "child-src";
33
34 const char kDirectiveSeparator[] = ";";
35
31 const char kPluginTypes[] = "plugin-types"; 36 const char kPluginTypes[] = "plugin-types";
32 37
33 const char kObjectSrcDefaultDirective[] = "object-src 'self';"; 38 const char kObjectSrcDefaultDirective[] = "object-src 'self';";
34 const char kScriptSrcDefaultDirective[] = 39 const char kScriptSrcDefaultDirective[] =
35 "script-src 'self' chrome-extension-resource:;"; 40 "script-src 'self' chrome-extension-resource:;";
36 41
42 const char kAppSandboxSubframeSrcDefaultDirective[] = "child-src 'self';";
43 const char kAppSandboxScriptSrcDefaultDirective[] =
44 "script-src 'self' 'unsafe-inline' 'unsafe-eval';";
45
37 const char kSandboxDirectiveName[] = "sandbox"; 46 const char kSandboxDirectiveName[] = "sandbox";
38 const char kAllowSameOriginToken[] = "allow-same-origin"; 47 const char kAllowSameOriginToken[] = "allow-same-origin";
39 const char kAllowTopNavigation[] = "allow-top-navigation"; 48 const char kAllowTopNavigation[] = "allow-top-navigation";
40 49
41 // This is the list of plugin types which are fully sandboxed and are safe to 50 // This is the list of plugin types which are fully sandboxed and are safe to
42 // load up in an extension, regardless of the URL they are navigated to. 51 // load up in an extension, regardless of the URL they are navigated to.
43 const char* const kSandboxedPluginTypes[] = { 52 const char* const kSandboxedPluginTypes[] = {
44 "application/pdf", 53 "application/pdf",
45 "application/x-google-chrome-pdf", 54 "application/x-google-chrome-pdf",
46 "application/x-pnacl" 55 "application/x-pnacl"
47 }; 56 };
48 57
49 // List of CSP hash-source prefixes that are accepted. Blink is a bit more 58 // List of CSP hash-source prefixes that are accepted. Blink is a bit more
50 // lenient, but we only accept standard hashes to be forward-compatible. 59 // lenient, but we only accept standard hashes to be forward-compatible.
51 // http://www.w3.org/TR/2015/CR-CSP2-20150721/#hash_algo 60 // http://www.w3.org/TR/2015/CR-CSP2-20150721/#hash_algo
52 const char* const kHashSourcePrefixes[] = { 61 const char* const kHashSourcePrefixes[] = {
53 "'sha256-", 62 "'sha256-",
54 "'sha384-", 63 "'sha384-",
55 "'sha512-" 64 "'sha512-"
56 }; 65 };
57 66
58 struct DirectiveStatus { 67 class DirectiveStatus {
59 explicit DirectiveStatus(const char* name) 68 public:
60 : directive_name(name), seen_in_policy(false) {} 69 DirectiveStatus(const char* name) { directive_names_.push_back(name); }
70 // Subframe related directives can have multiple directive names: "child-src"
71 // or "frame-src".
72 DirectiveStatus(const char* name1, const char* name2) {
73 directive_names_.push_back(name1);
74 directive_names_.push_back(name2);
75 }
76 bool Matches(const std::string& directive_name) const {
77 for (const auto& directive : directive_names_) {
78 if (directive_name == directive)
79 return true;
80 }
81 return false;
82 }
83 bool seen_in_policy() const { return seen_in_policy_; }
84 void set_seen_in_policy() { seen_in_policy_ = true; }
61 85
62 const char* directive_name; 86 std::string name() const {
63 bool seen_in_policy; 87 DCHECK(!directive_names_.empty());
88 return directive_names_[0];
89 }
90
91 private:
92 std::vector<const char*> directive_names_;
93 bool seen_in_policy_ = false;
64 }; 94 };
65 95
66 // Returns whether |url| starts with |scheme_and_separator| and does not have a 96 // Returns whether |url| starts with |scheme_and_separator| and does not have a
67 // too permissive wildcard host name. If |should_check_rcd| is true, then the 97 // too permissive wildcard host name. If |should_check_rcd| is true, then the
68 // Public suffix list is used to exclude wildcard TLDs such as "https://*.org". 98 // Public suffix list is used to exclude wildcard TLDs such as "https://*.org".
69 bool isNonWildcardTLD(const std::string& url, 99 bool isNonWildcardTLD(const std::string& url,
70 const std::string& scheme_and_separator, 100 const std::string& scheme_and_separator,
71 bool should_check_rcd) { 101 bool should_check_rcd) {
72 if (!base::StartsWith(url, scheme_and_separator, 102 if (!base::StartsWith(url, scheme_and_separator,
73 base::CompareCase::SENSITIVE)) 103 base::CompareCase::SENSITIVE))
(...skipping 118 matching lines...) Expand 10 before | Expand all | Expand 10 after
192 manifest_errors::kInvalidCSPInsecureValue, source_literal, 222 manifest_errors::kInvalidCSPInsecureValue, source_literal,
193 directive_name))); 223 directive_name)));
194 } 224 }
195 } 225 }
196 // End of CSP directive that was started at the beginning of this method. If 226 // End of CSP directive that was started at the beginning of this method. If
197 // none of the values are secure, the policy will be empty and default to 227 // none of the values are secure, the policy will be empty and default to
198 // 'none', which is secure. 228 // 'none', which is secure.
199 sane_csp_parts->back().push_back(';'); 229 sane_csp_parts->back().push_back(';');
200 } 230 }
201 231
202 // Returns true if |directive_name| matches |status.directive_name|.
203 bool UpdateStatus(const std::string& directive_name,
204 base::StringTokenizer* tokenizer,
205 DirectiveStatus* status,
206 int options,
207 std::vector<std::string>* sane_csp_parts,
208 std::vector<InstallWarning>* warnings) {
209 if (directive_name != status->directive_name)
210 return false;
211
212 if (!status->seen_in_policy) {
213 status->seen_in_policy = true;
214 GetSecureDirectiveValues(directive_name, tokenizer, options, sane_csp_parts,
215 warnings);
216 } else {
217 // Don't show any errors for duplicate CSP directives, because it will be
218 // ignored by the CSP parser (http://www.w3.org/TR/CSP2/#policy-parsing).
219 GetSecureDirectiveValues(directive_name, tokenizer, options, sane_csp_parts,
220 NULL);
221 }
222 return true;
223 }
224
225 // Returns true if the |plugin_type| is one of the fully sandboxed plugin types. 232 // Returns true if the |plugin_type| is one of the fully sandboxed plugin types.
226 bool PluginTypeAllowed(const std::string& plugin_type) { 233 bool PluginTypeAllowed(const std::string& plugin_type) {
227 for (size_t i = 0; i < arraysize(kSandboxedPluginTypes); ++i) { 234 for (size_t i = 0; i < arraysize(kSandboxedPluginTypes); ++i) {
228 if (plugin_type == kSandboxedPluginTypes[i]) 235 if (plugin_type == kSandboxedPluginTypes[i])
229 return true; 236 return true;
230 } 237 }
231 return false; 238 return false;
232 } 239 }
233 240
234 // Returns true if the policy is allowed to contain an insecure object-src 241 // Returns true if the policy is allowed to contain an insecure object-src
(...skipping 17 matching lines...) Expand all
252 if (!PluginTypeAllowed(tokenizer.token())) 259 if (!PluginTypeAllowed(tokenizer.token()))
253 return false; 260 return false;
254 } 261 }
255 // All listed plugin types are whitelisted. 262 // All listed plugin types are whitelisted.
256 return true; 263 return true;
257 } 264 }
258 // plugin-types not specified. 265 // plugin-types not specified.
259 return false; 266 return false;
260 } 267 }
261 268
269 class CSPEnforcer {
270 public:
271 CSPEnforcer(bool show_missing_csp_warnings)
272 : show_missing_csp_warnings_(show_missing_csp_warnings) {}
273 virtual ~CSPEnforcer() {}
274
275 std::string Enforce(const std::string& policy,
276 std::vector<InstallWarning>* warnings);
277
278 protected:
279 virtual std::string GetDefaultCSPValue(const DirectiveStatus& status) = 0;
280
281 // Updates the status of a directive |directive_status| with given information
282 // about a directive token. The directive token has name |directive_name| and
283 // its values are in |tokenizer|.
284 //
285 // Returns true if |directive_status| matches |directive_name|.
286 virtual bool VisitAndEnforce(const std::string& directive_name,
287 DirectiveStatus* directive_status,
288 base::StringTokenizer* tokenizer,
289 std::vector<InstallWarning>* warnings) = 0;
290
291 std::vector<DirectiveStatus> directives_;
292 std::vector<std::string> enforced_csp_parts_;
293 const bool show_missing_csp_warnings_;
294 };
295
296 std::string CSPEnforcer::Enforce(const std::string& policy,
297 std::vector<InstallWarning>* warnings) {
Devlin 2016/12/15 00:04:11 As discussed briefly offline, I still find this pr
lazyboy 2016/12/15 08:02:35 I think there are too many cases to consider here
298 enforced_csp_parts_.clear();
299
300 // If any directive that we care about isn't explicitly listed in |policy|,
301 // "default-src" fallback is used.
302 DirectiveStatus default_src_status(kDefaultSrc);
303 std::vector<InstallWarning> default_src_csp_warnings;
304
305 // See http://www.w3.org/TR/CSP/#parse-a-csp-policy for parsing algorithm.
306 for (const std::string& directive : base::SplitString(
307 policy, ";", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL)) {
308 base::StringTokenizer tokenizer(directive, " \t\r\n");
309 if (!tokenizer.GetNext())
310 continue;
311
312 std::string directive_name = base::ToLowerASCII(tokenizer.token_piece());
313 bool matches_enforcing_directive = false;
314 for (auto& directive_status : directives_) {
315 if (VisitAndEnforce(directive_name, &directive_status, &tokenizer,
316 warnings)) {
317 matches_enforcing_directive = true;
318 break;
319 }
320 }
321 if (matches_enforcing_directive)
322 continue;
323
324 if (VisitAndEnforce(directive_name, &default_src_status, &tokenizer,
325 &default_src_csp_warnings)) {
326 continue;
327 }
328
329 // Keep this directive as is.
330 enforced_csp_parts_.push_back(directive + kDirectiveSeparator);
331 }
332
333 if (default_src_status.seen_in_policy()) {
334 for (const DirectiveStatus& directive_status : directives_) {
335 if (!directive_status.seen_in_policy()) {
336 // This |directive_status| falls back to "default-src". So warnings from
337 // "default-src" will apply.
338 if (warnings) {
339 warnings->insert(warnings->end(), default_src_csp_warnings.begin(),
340 default_src_csp_warnings.end());
341 }
342 break;
343 }
344 }
345 } else {
346 // Did not see "default-src".
347 // Make sure we cover all sources from |directives_|.
348 for (const DirectiveStatus& directive_status : directives_) {
349 if (directive_status.seen_in_policy()) // Already covered.
350 continue;
351 enforced_csp_parts_.push_back(GetDefaultCSPValue(directive_status));
352
353 if (warnings && show_missing_csp_warnings_) {
354 warnings->push_back(CSPInstallWarning(ErrorUtils::FormatErrorMessage(
355 manifest_errors::kInvalidCSPMissingSecureSrc,
356 directive_status.name())));
357 }
358 }
359 }
360
361 return base::JoinString(enforced_csp_parts_, " ");
362 }
363
364 class ExtensionCSPEnforcer : public CSPEnforcer {
365 public:
366 ExtensionCSPEnforcer(bool allow_insecure_object_src, int options)
367 : CSPEnforcer(true), options_(options) {
368 directives_.push_back(DirectiveStatus(kScriptSrc));
369 if (!allow_insecure_object_src)
370 directives_.push_back(DirectiveStatus(kObjectSrc));
371 }
372
373 protected:
374 bool VisitAndEnforce(const std::string& directive_name,
375 DirectiveStatus* directive_status,
376 base::StringTokenizer* tokenizer,
377 std::vector<InstallWarning>* warnings) override {
378 if (!directive_status->Matches(directive_name))
379 return false;
380
381 if (!directive_status->seen_in_policy()) {
382 directive_status->set_seen_in_policy();
383 GetSecureDirectiveValues(directive_name, tokenizer, options_,
384 &enforced_csp_parts_, warnings);
385 } else {
386 // Don't show any errors for duplicate CSP directives, because it will be
387 // ignored by the CSP parser (http://www.w3.org/TR/CSP2/#policy-parsing).
388 GetSecureDirectiveValues(directive_name, tokenizer, options_,
389 &enforced_csp_parts_, nullptr);
390 }
391 return true;
392 }
393
394 std::string GetDefaultCSPValue(const DirectiveStatus& status) override {
395 if (status.Matches(kObjectSrc))
396 return kObjectSrcDefaultDirective;
397 DCHECK(status.Matches(kScriptSrc));
398 return kScriptSrcDefaultDirective;
399 }
400
401 private:
402 const int options_;
403 };
404
405 class AppSandboxPageCSPEnforcer : public CSPEnforcer {
406 public:
407 AppSandboxPageCSPEnforcer() : CSPEnforcer(false) {
408 directives_.push_back(DirectiveStatus(kChildSrc, kFrameSrc));
409 directives_.push_back(DirectiveStatus(kScriptSrc));
410 }
411
412 protected:
413 bool VisitAndEnforce(const std::string& directive_name,
414 DirectiveStatus* directive_status,
415 base::StringTokenizer* tokenizer,
416 std::vector<InstallWarning>* warnings) override {
417 if (!directive_status->Matches(directive_name))
418 return false;
419
420 // Don't show any errors for duplicate CSP directives, because it will be
421 // ignored by the CSP parser (http://www.w3.org/TR/CSP2/#policy-parsing).
422 bool is_duplicate_directive = directive_status->seen_in_policy();
423 directive_status->set_seen_in_policy();
424
425 enforced_csp_parts_.push_back(directive_name);
426 bool seen_self_or_none = false;
427 while (tokenizer->GetNext()) {
428 std::string source_literal = tokenizer->token();
429 std::string source_lower = base::ToLowerASCII(source_literal);
430
431 // Keyword directive sources are surrounded with quotes, e.g. 'self',
432 // 'sha256-...', 'unsafe-eval', 'nonce-...'. These do not specify a remote
433 // host or '*', so keep them and restrict the rest.
434 if (source_lower.size() > 1u && source_lower[0] == '\'' &&
435 source_lower[source_lower.size() - 1] == '\'') {
436 if (source_lower == "'none'" || source_lower == "'self'")
437 seen_self_or_none |= true;
438 enforced_csp_parts_.push_back(source_lower);
439 } else if (warnings && !is_duplicate_directive) {
440 warnings->push_back(CSPInstallWarning(ErrorUtils::FormatErrorMessage(
441 manifest_errors::kInvalidCSPInsecureValue, source_literal,
442 directive_name)));
443 }
444 }
445
446 if (!seen_self_or_none)
447 enforced_csp_parts_.push_back("'self'");
448
449 // Add ";" at the end of the directive.
450 enforced_csp_parts_.back().append(kDirectiveSeparator);
451 return true;
452 }
453
454 std::string GetDefaultCSPValue(const DirectiveStatus& status) override {
455 if (status.Matches(kChildSrc))
456 return kAppSandboxSubframeSrcDefaultDirective;
457 DCHECK(status.Matches(kScriptSrc));
458 return kAppSandboxScriptSrcDefaultDirective;
459 }
460 };
461
262 } // namespace 462 } // namespace
263 463
264 bool ContentSecurityPolicyIsLegal(const std::string& policy) { 464 bool ContentSecurityPolicyIsLegal(const std::string& policy) {
265 // We block these characters to prevent HTTP header injection when 465 // We block these characters to prevent HTTP header injection when
266 // representing the content security policy as an HTTP header. 466 // representing the content security policy as an HTTP header.
267 const char kBadChars[] = {',', '\r', '\n', '\0'}; 467 const char kBadChars[] = {',', '\r', '\n', '\0'};
268 468
269 return policy.find_first_of(kBadChars, 0, arraysize(kBadChars)) == 469 return policy.find_first_of(kBadChars, 0, arraysize(kBadChars)) ==
270 std::string::npos; 470 std::string::npos;
271 } 471 }
272 472
273 std::string SanitizeContentSecurityPolicy( 473 std::string SanitizeContentSecurityPolicy(
274 const std::string& policy, 474 const std::string& policy,
275 int options, 475 int options,
276 std::vector<InstallWarning>* warnings) { 476 std::vector<InstallWarning>* warnings) {
277 // See http://www.w3.org/TR/CSP/#parse-a-csp-policy for parsing algorithm. 477 // See http://www.w3.org/TR/CSP/#parse-a-csp-policy for parsing algorithm.
278 std::vector<std::string> directives = base::SplitString( 478 std::vector<std::string> directives = base::SplitString(
279 policy, ";", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); 479 policy, kDirectiveSeparator, base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
280
281 DirectiveStatus default_src_status(kDefaultSrc);
282 DirectiveStatus script_src_status(kScriptSrc);
283 DirectiveStatus object_src_status(kObjectSrc);
284 480
285 bool allow_insecure_object_src = 481 bool allow_insecure_object_src =
286 AllowedToHaveInsecureObjectSrc(options, directives); 482 AllowedToHaveInsecureObjectSrc(options, directives);
287 483
288 std::vector<std::string> sane_csp_parts; 484 ExtensionCSPEnforcer csp_enforcer(allow_insecure_object_src, options);
289 std::vector<InstallWarning> default_src_csp_warnings; 485 return csp_enforcer.Enforce(policy, warnings);
290 for (size_t i = 0; i < directives.size(); ++i) { 486 }
291 std::string& input = directives[i]; 487
292 base::StringTokenizer tokenizer(input, " \t\r\n"); 488 std::string GetEffectiveSandoxedPageCSP(const std::string& policy,
293 if (!tokenizer.GetNext()) 489 std::vector<InstallWarning>* warnings) {
294 continue; 490 AppSandboxPageCSPEnforcer csp_enforcer;
295 491 return csp_enforcer.Enforce(policy, warnings);
296 std::string directive_name = base::ToLowerASCII(tokenizer.token_piece());
297 if (UpdateStatus(directive_name, &tokenizer, &default_src_status, options,
298 &sane_csp_parts, &default_src_csp_warnings))
299 continue;
300 if (UpdateStatus(directive_name, &tokenizer, &script_src_status, options,
301 &sane_csp_parts, warnings))
302 continue;
303 if (!allow_insecure_object_src &&
304 UpdateStatus(directive_name, &tokenizer, &object_src_status, options,
305 &sane_csp_parts, warnings))
306 continue;
307
308 // Pass the other CSP directives as-is without further validation.
309 sane_csp_parts.push_back(input + ";");
310 }
311
312 if (default_src_status.seen_in_policy) {
313 if (!script_src_status.seen_in_policy ||
314 !object_src_status.seen_in_policy) {
315 // Insecure values in default-src are only relevant if either script-src
316 // or object-src is omitted.
317 if (warnings)
318 warnings->insert(warnings->end(),
319 default_src_csp_warnings.begin(),
320 default_src_csp_warnings.end());
321 }
322 } else {
323 if (!script_src_status.seen_in_policy) {
324 sane_csp_parts.push_back(kScriptSrcDefaultDirective);
325 if (warnings)
326 warnings->push_back(CSPInstallWarning(ErrorUtils::FormatErrorMessage(
327 manifest_errors::kInvalidCSPMissingSecureSrc, kScriptSrc)));
328 }
329 if (!object_src_status.seen_in_policy && !allow_insecure_object_src) {
330 sane_csp_parts.push_back(kObjectSrcDefaultDirective);
331 if (warnings)
332 warnings->push_back(CSPInstallWarning(ErrorUtils::FormatErrorMessage(
333 manifest_errors::kInvalidCSPMissingSecureSrc, kObjectSrc)));
334 }
335 }
336
337 return base::JoinString(sane_csp_parts, " ");
338 } 492 }
339 493
340 bool ContentSecurityPolicyIsSandboxed( 494 bool ContentSecurityPolicyIsSandboxed(
341 const std::string& policy, Manifest::Type type) { 495 const std::string& policy, Manifest::Type type) {
342 // See http://www.w3.org/TR/CSP/#parse-a-csp-policy for parsing algorithm. 496 // See http://www.w3.org/TR/CSP/#parse-a-csp-policy for parsing algorithm.
343 bool seen_sandbox = false; 497 bool seen_sandbox = false;
344 for (const std::string& input : base::SplitString( 498 for (const std::string& input : base::SplitString(
345 policy, ";", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL)) { 499 policy, ";", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL)) {
346 base::StringTokenizer tokenizer(input, " \t\r\n"); 500 base::StringTokenizer tokenizer(input, " \t\r\n");
347 if (!tokenizer.GetNext()) 501 if (!tokenizer.GetNext())
(...skipping 19 matching lines...) Expand all
367 } 521 }
368 } 522 }
369 } 523 }
370 524
371 return seen_sandbox; 525 return seen_sandbox;
372 } 526 }
373 527
374 } // namespace csp_validator 528 } // namespace csp_validator
375 529
376 } // namespace extensions 530 } // namespace extensions
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698