OLD | NEW |
---|---|
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 Loading... | |
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 Loading... | |
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 Loading... | |
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 Loading... | |
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 |
OLD | NEW |