| 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 <vector> | 7 #include <vector> |
| 8 | 8 |
| 9 #include "base/strings/string_split.h" | 9 #include "base/strings/string_split.h" |
| 10 #include "base/strings/string_tokenizer.h" | 10 #include "base/strings/string_tokenizer.h" |
| 11 #include "base/strings/string_util.h" | 11 #include "base/strings/string_util.h" |
| 12 #include "content/public/common/url_constants.h" | 12 #include "content/public/common/url_constants.h" |
| 13 #include "extensions/common/constants.h" | 13 #include "extensions/common/constants.h" |
| 14 #include "extensions/common/error_utils.h" |
| 15 #include "extensions/common/install_warning.h" |
| 16 #include "extensions/common/manifest_constants.h" |
| 14 #include "net/base/registry_controlled_domains/registry_controlled_domain.h" | 17 #include "net/base/registry_controlled_domains/registry_controlled_domain.h" |
| 15 | 18 |
| 16 namespace extensions { | 19 namespace extensions { |
| 17 | 20 |
| 18 namespace csp_validator { | 21 namespace csp_validator { |
| 19 | 22 |
| 20 namespace { | 23 namespace { |
| 21 | 24 |
| 22 const char kDefaultSrc[] = "default-src"; | 25 const char kDefaultSrc[] = "default-src"; |
| 23 const char kScriptSrc[] = "script-src"; | 26 const char kScriptSrc[] = "script-src"; |
| 24 const char kObjectSrc[] = "object-src"; | 27 const char kObjectSrc[] = "object-src"; |
| 25 const char kPluginTypes[] = "plugin-types"; | 28 const char kPluginTypes[] = "plugin-types"; |
| 26 | 29 |
| 30 const char kObjectSrcDefaultDirective[] = "object-src 'self';"; |
| 31 const char kScriptSrcDefaultDirective[] = |
| 32 "script-src 'self' chrome-extension-resource:;"; |
| 33 |
| 27 const char kSandboxDirectiveName[] = "sandbox"; | 34 const char kSandboxDirectiveName[] = "sandbox"; |
| 28 const char kAllowSameOriginToken[] = "allow-same-origin"; | 35 const char kAllowSameOriginToken[] = "allow-same-origin"; |
| 29 const char kAllowTopNavigation[] = "allow-top-navigation"; | 36 const char kAllowTopNavigation[] = "allow-top-navigation"; |
| 30 | 37 |
| 31 // This is the list of plugin types which are fully sandboxed and are safe to | 38 // This is the list of plugin types which are fully sandboxed and are safe to |
| 32 // load up in an extension, regardless of the URL they are navigated to. | 39 // load up in an extension, regardless of the URL they are navigated to. |
| 33 const char* const kSandboxedPluginTypes[] = { | 40 const char* const kSandboxedPluginTypes[] = { |
| 34 "application/pdf", | 41 "application/pdf", |
| 35 "application/x-google-chrome-pdf", | 42 "application/x-google-chrome-pdf", |
| 36 "application/x-pnacl" | 43 "application/x-pnacl" |
| 37 }; | 44 }; |
| 38 | 45 |
| 39 struct DirectiveStatus { | 46 struct DirectiveStatus { |
| 40 explicit DirectiveStatus(const char* name) | 47 explicit DirectiveStatus(const char* name) |
| 41 : directive_name(name) | 48 : directive_name(name), seen_in_policy(false) {} |
| 42 , seen_in_policy(false) | |
| 43 , is_secure(false) { | |
| 44 } | |
| 45 | 49 |
| 46 const char* directive_name; | 50 const char* directive_name; |
| 47 bool seen_in_policy; | 51 bool seen_in_policy; |
| 48 bool is_secure; | |
| 49 }; | 52 }; |
| 50 | 53 |
| 51 // Returns whether |url| starts with |scheme_and_separator| and does not have a | 54 // Returns whether |url| starts with |scheme_and_separator| and does not have a |
| 52 // too permissive wildcard host name. If |should_check_rcd| is true, then the | 55 // too permissive wildcard host name. If |should_check_rcd| is true, then the |
| 53 // Public suffix list is used to exclude wildcard TLDs such as "https://*.org". | 56 // Public suffix list is used to exclude wildcard TLDs such as "https://*.org". |
| 54 bool isNonWildcardTLD(const std::string& url, | 57 bool isNonWildcardTLD(const std::string& url, |
| 55 const std::string& scheme_and_separator, | 58 const std::string& scheme_and_separator, |
| 56 bool should_check_rcd) { | 59 bool should_check_rcd) { |
| 57 if (!StartsWithASCII(url, scheme_and_separator, true)) | 60 if (!StartsWithASCII(url, scheme_and_separator, true)) |
| 58 return false; | 61 return false; |
| 59 | 62 |
| 60 size_t start_of_host = scheme_and_separator.length(); | 63 size_t start_of_host = scheme_and_separator.length(); |
| 61 | 64 |
| 62 size_t end_of_host = url.find("/", start_of_host); | 65 size_t end_of_host = url.find("/", start_of_host); |
| 63 if (end_of_host == std::string::npos) | 66 if (end_of_host == std::string::npos) |
| 64 end_of_host = url.size(); | 67 end_of_host = url.size(); |
| 65 | 68 |
| 66 // A missing host such as "chrome-extension://" is invalid, but for backwards- | |
| 67 // compatibility, accept such CSP parts. They will be ignored by Blink anyway. | |
| 68 // TODO(robwu): Remove this special case once crbug.com/434773 is fixed. | |
| 69 if (start_of_host == end_of_host) | |
| 70 return true; | |
| 71 | |
| 72 // Note: It is sufficient to only compare the first character against '*' | 69 // Note: It is sufficient to only compare the first character against '*' |
| 73 // because the CSP only allows wildcards at the start of a directive, see | 70 // because the CSP only allows wildcards at the start of a directive, see |
| 74 // host-source and host-part at http://www.w3.org/TR/CSP2/#source-list-syntax | 71 // host-source and host-part at http://www.w3.org/TR/CSP2/#source-list-syntax |
| 75 bool is_wildcard_subdomain = end_of_host > start_of_host + 2 && | 72 bool is_wildcard_subdomain = end_of_host > start_of_host + 2 && |
| 76 url[start_of_host] == '*' && url[start_of_host + 1] == '.'; | 73 url[start_of_host] == '*' && url[start_of_host + 1] == '.'; |
| 77 if (is_wildcard_subdomain) | 74 if (is_wildcard_subdomain) |
| 78 start_of_host += 2; | 75 start_of_host += 2; |
| 79 | 76 |
| 80 size_t start_of_port = url.rfind(":", end_of_host); | 77 size_t start_of_port = url.rfind(":", end_of_host); |
| 81 // The ":" check at the end of the following condition is used to avoid | 78 // The ":" check at the end of the following condition is used to avoid |
| (...skipping 26 matching lines...) Expand all Loading... |
| 108 return true; | 105 return true; |
| 109 | 106 |
| 110 // Wildcards on subdomains of a TLD are not allowed. | 107 // Wildcards on subdomains of a TLD are not allowed. |
| 111 size_t registry_length = net::registry_controlled_domains::GetRegistryLength( | 108 size_t registry_length = net::registry_controlled_domains::GetRegistryLength( |
| 112 host, | 109 host, |
| 113 net::registry_controlled_domains::INCLUDE_UNKNOWN_REGISTRIES, | 110 net::registry_controlled_domains::INCLUDE_UNKNOWN_REGISTRIES, |
| 114 net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES); | 111 net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES); |
| 115 return registry_length != 0; | 112 return registry_length != 0; |
| 116 } | 113 } |
| 117 | 114 |
| 118 bool HasOnlySecureTokens(base::StringTokenizer& tokenizer, | 115 InstallWarning CSPInstallWarning(const std::string& csp_warning) { |
| 119 int options) { | 116 return InstallWarning(csp_warning, manifest_keys::kContentSecurityPolicy); |
| 120 while (tokenizer.GetNext()) { | 117 } |
| 121 std::string source = tokenizer.token(); | 118 |
| 119 void GetSecureDirectiveValues(const std::string& directive_name, |
| 120 base::StringTokenizer* tokenizer, |
| 121 int options, |
| 122 std::vector<std::string>* sane_csp_parts, |
| 123 std::vector<InstallWarning>* warnings) { |
| 124 sane_csp_parts->push_back(directive_name); |
| 125 while (tokenizer->GetNext()) { |
| 126 std::string source = tokenizer->token(); |
| 122 base::StringToLowerASCII(&source); | 127 base::StringToLowerASCII(&source); |
| 128 bool is_secure_csp_token = false; |
| 123 | 129 |
| 124 // We might need to relax this whitelist over time. | 130 // We might need to relax this whitelist over time. |
| 125 if (source == "'self'" || | 131 if (source == "'self'" || |
| 126 source == "'none'" || | 132 source == "'none'" || |
| 127 source == "http://127.0.0.1" || | 133 source == "http://127.0.0.1" || |
| 128 LowerCaseEqualsASCII(source, "blob:") || | 134 LowerCaseEqualsASCII(source, "blob:") || |
| 129 LowerCaseEqualsASCII(source, "filesystem:") || | 135 LowerCaseEqualsASCII(source, "filesystem:") || |
| 130 LowerCaseEqualsASCII(source, "http://localhost") || | 136 LowerCaseEqualsASCII(source, "http://localhost") || |
| 131 StartsWithASCII(source, "http://127.0.0.1:", true) || | 137 StartsWithASCII(source, "http://127.0.0.1:", true) || |
| 132 StartsWithASCII(source, "http://localhost:", true) || | 138 StartsWithASCII(source, "http://localhost:", true) || |
| 133 isNonWildcardTLD(source, "https://", true) || | 139 isNonWildcardTLD(source, "https://", true) || |
| 134 isNonWildcardTLD(source, "chrome://", false) || | 140 isNonWildcardTLD(source, "chrome://", false) || |
| 135 isNonWildcardTLD(source, | 141 isNonWildcardTLD(source, |
| 136 std::string(extensions::kExtensionScheme) + | 142 std::string(extensions::kExtensionScheme) + |
| 137 url::kStandardSchemeSeparator, | 143 url::kStandardSchemeSeparator, |
| 138 false) || | 144 false) || |
| 139 StartsWithASCII(source, "chrome-extension-resource:", true)) { | 145 StartsWithASCII(source, "chrome-extension-resource:", true)) { |
| 140 continue; | 146 is_secure_csp_token = true; |
| 147 } else if ((options & OPTIONS_ALLOW_UNSAFE_EVAL) && |
| 148 source == "'unsafe-eval'") { |
| 149 is_secure_csp_token = true; |
| 141 } | 150 } |
| 142 | 151 |
| 143 if (options & OPTIONS_ALLOW_UNSAFE_EVAL) { | 152 if (is_secure_csp_token) { |
| 144 if (source == "'unsafe-eval'") | 153 sane_csp_parts->push_back(source); |
| 145 continue; | 154 } else if (warnings) { |
| 155 warnings->push_back(CSPInstallWarning(ErrorUtils::FormatErrorMessage( |
| 156 manifest_errors::kInvalidCSPInsecureValue, source, directive_name))); |
| 146 } | 157 } |
| 147 | |
| 148 return false; | |
| 149 } | 158 } |
| 150 | 159 // End of CSP directive that was started at the beginning of this method. If |
| 151 return true; // Empty values default to 'none', which is secure. | 160 // none of the values are secure, the policy will be empty and default to |
| 161 // 'none', which is secure. |
| 162 sane_csp_parts->back().push_back(';'); |
| 152 } | 163 } |
| 153 | 164 |
| 154 // Returns true if |directive_name| matches |status.directive_name|. | 165 // Returns true if |directive_name| matches |status.directive_name|. |
| 155 bool UpdateStatus(const std::string& directive_name, | 166 bool UpdateStatus(const std::string& directive_name, |
| 156 base::StringTokenizer& tokenizer, | 167 base::StringTokenizer* tokenizer, |
| 157 DirectiveStatus* status, | 168 DirectiveStatus* status, |
| 158 int options) { | 169 int options, |
| 159 if (status->seen_in_policy) | 170 std::vector<std::string>* sane_csp_parts, |
| 160 return false; | 171 std::vector<InstallWarning>* warnings) { |
| 161 if (directive_name != status->directive_name) | 172 if (directive_name != status->directive_name) |
| 162 return false; | 173 return false; |
| 163 status->seen_in_policy = true; | 174 |
| 164 status->is_secure = HasOnlySecureTokens(tokenizer, options); | 175 if (!status->seen_in_policy) { |
| 176 status->seen_in_policy = true; |
| 177 GetSecureDirectiveValues(directive_name, tokenizer, options, sane_csp_parts, |
| 178 warnings); |
| 179 } else { |
| 180 // Don't show any errors for duplicate CSP directives, because it will be |
| 181 // ignored by the CSP parser (http://www.w3.org/TR/CSP2/#policy-parsing). |
| 182 GetSecureDirectiveValues(directive_name, tokenizer, options, sane_csp_parts, |
| 183 NULL); |
| 184 } |
| 165 return true; | 185 return true; |
| 166 } | 186 } |
| 167 | 187 |
| 168 // Parses the plugin-types directive and returns the list of mime types | |
| 169 // specified in |plugin_types|. | |
| 170 bool ParsePluginTypes(const std::string& directive_name, | |
| 171 base::StringTokenizer& tokenizer, | |
| 172 std::vector<std::string>* plugin_types) { | |
| 173 DCHECK(plugin_types); | |
| 174 | |
| 175 if (directive_name != kPluginTypes || !plugin_types->empty()) | |
| 176 return false; | |
| 177 | |
| 178 while (tokenizer.GetNext()) { | |
| 179 std::string mime_type = tokenizer.token(); | |
| 180 base::StringToLowerASCII(&mime_type); | |
| 181 // Since we're comparing the mime types to a whitelist, we don't check them | |
| 182 // for strict validity right now. | |
| 183 plugin_types->push_back(mime_type); | |
| 184 } | |
| 185 | |
| 186 return true; | |
| 187 } | |
| 188 | |
| 189 // Returns true if the |plugin_type| is one of the fully sandboxed plugin types. | 188 // Returns true if the |plugin_type| is one of the fully sandboxed plugin types. |
| 190 bool PluginTypeAllowed(const std::string& plugin_type) { | 189 bool PluginTypeAllowed(const std::string& plugin_type) { |
| 191 for (size_t i = 0; i < arraysize(kSandboxedPluginTypes); ++i) { | 190 for (size_t i = 0; i < arraysize(kSandboxedPluginTypes); ++i) { |
| 192 if (plugin_type == kSandboxedPluginTypes[i]) | 191 if (plugin_type == kSandboxedPluginTypes[i]) |
| 193 return true; | 192 return true; |
| 194 } | 193 } |
| 195 return false; | 194 return false; |
| 196 } | 195 } |
| 197 | 196 |
| 198 // Returns true if the policy is allowed to contain an insecure object-src | 197 // Returns true if the policy is allowed to contain an insecure object-src |
| 199 // directive. This requires OPTIONS_ALLOW_INSECURE_OBJECT_SRC to be specified | 198 // directive. This requires OPTIONS_ALLOW_INSECURE_OBJECT_SRC to be specified |
| 200 // as an option and the plugin-types that can be loaded must be restricted to | 199 // as an option and the plugin-types that can be loaded must be restricted to |
| 201 // the set specified in kSandboxedPluginTypes. | 200 // the set specified in kSandboxedPluginTypes. |
| 202 bool AllowedToHaveInsecureObjectSrc( | 201 bool AllowedToHaveInsecureObjectSrc( |
| 203 int options, | 202 int options, |
| 204 const std::vector<std::string>& plugin_types) { | 203 const std::vector<std::string>& directives) { |
| 205 if (!(options & OPTIONS_ALLOW_INSECURE_OBJECT_SRC)) | 204 if (!(options & OPTIONS_ALLOW_INSECURE_OBJECT_SRC)) |
| 206 return false; | 205 return false; |
| 207 | 206 |
| 208 // plugin-types must be specified. | 207 for (size_t i = 0; i < directives.size(); ++i) { |
| 209 if (plugin_types.empty()) | 208 const std::string& input = directives[i]; |
| 210 return false; | 209 base::StringTokenizer tokenizer(input, " \t\r\n"); |
| 211 | 210 if (!tokenizer.GetNext()) |
| 212 for (const auto& plugin_type : plugin_types) { | 211 continue; |
| 213 if (!PluginTypeAllowed(plugin_type)) | 212 if (!LowerCaseEqualsASCII(tokenizer.token(), kPluginTypes)) |
| 214 return false; | 213 continue; |
| 214 while (tokenizer.GetNext()) { |
| 215 if (!PluginTypeAllowed(tokenizer.token())) |
| 216 return false; |
| 217 } |
| 218 // All listed plugin types are whitelisted. |
| 219 return true; |
| 215 } | 220 } |
| 216 | 221 // plugin-types not specified. |
| 217 return true; | 222 return false; |
| 218 } | 223 } |
| 219 | 224 |
| 220 } // namespace | 225 } // namespace |
| 221 | 226 |
| 222 bool ContentSecurityPolicyIsLegal(const std::string& policy) { | 227 bool ContentSecurityPolicyIsLegal(const std::string& policy) { |
| 223 // We block these characters to prevent HTTP header injection when | 228 // We block these characters to prevent HTTP header injection when |
| 224 // representing the content security policy as an HTTP header. | 229 // representing the content security policy as an HTTP header. |
| 225 const char kBadChars[] = {',', '\r', '\n', '\0'}; | 230 const char kBadChars[] = {',', '\r', '\n', '\0'}; |
| 226 | 231 |
| 227 return policy.find_first_of(kBadChars, 0, arraysize(kBadChars)) == | 232 return policy.find_first_of(kBadChars, 0, arraysize(kBadChars)) == |
| 228 std::string::npos; | 233 std::string::npos; |
| 229 } | 234 } |
| 230 | 235 |
| 231 bool ContentSecurityPolicyIsSecure(const std::string& policy, | 236 std::string SanitizeContentSecurityPolicy( |
| 232 int options) { | 237 const std::string& policy, |
| 238 int options, |
| 239 std::vector<InstallWarning>* warnings) { |
| 233 // See http://www.w3.org/TR/CSP/#parse-a-csp-policy for parsing algorithm. | 240 // See http://www.w3.org/TR/CSP/#parse-a-csp-policy for parsing algorithm. |
| 234 std::vector<std::string> directives; | 241 std::vector<std::string> directives; |
| 235 base::SplitString(policy, ';', &directives); | 242 base::SplitString(policy, ';', &directives); |
| 236 | 243 |
| 237 DirectiveStatus default_src_status(kDefaultSrc); | 244 DirectiveStatus default_src_status(kDefaultSrc); |
| 238 DirectiveStatus script_src_status(kScriptSrc); | 245 DirectiveStatus script_src_status(kScriptSrc); |
| 239 DirectiveStatus object_src_status(kObjectSrc); | 246 DirectiveStatus object_src_status(kObjectSrc); |
| 240 | 247 |
| 241 std::vector<std::string> plugin_types; | 248 bool allow_insecure_object_src = |
| 249 AllowedToHaveInsecureObjectSrc(options, directives); |
| 242 | 250 |
| 251 std::vector<std::string> sane_csp_parts; |
| 252 std::vector<InstallWarning> default_src_csp_warnings; |
| 243 for (size_t i = 0; i < directives.size(); ++i) { | 253 for (size_t i = 0; i < directives.size(); ++i) { |
| 244 std::string& input = directives[i]; | 254 std::string& input = directives[i]; |
| 245 base::StringTokenizer tokenizer(input, " \t\r\n"); | 255 base::StringTokenizer tokenizer(input, " \t\r\n"); |
| 246 if (!tokenizer.GetNext()) | 256 if (!tokenizer.GetNext()) |
| 247 continue; | 257 continue; |
| 248 | 258 |
| 249 std::string directive_name = tokenizer.token(); | 259 std::string directive_name = tokenizer.token(); |
| 250 base::StringToLowerASCII(&directive_name); | 260 base::StringToLowerASCII(&directive_name); |
| 251 | 261 |
| 252 if (UpdateStatus(directive_name, tokenizer, &default_src_status, options)) | 262 if (UpdateStatus(directive_name, &tokenizer, &default_src_status, options, |
| 263 &sane_csp_parts, &default_src_csp_warnings)) |
| 253 continue; | 264 continue; |
| 254 if (UpdateStatus(directive_name, tokenizer, &script_src_status, options)) | 265 if (UpdateStatus(directive_name, &tokenizer, &script_src_status, options, |
| 266 &sane_csp_parts, warnings)) |
| 255 continue; | 267 continue; |
| 256 if (UpdateStatus(directive_name, tokenizer, &object_src_status, options)) | 268 if (!allow_insecure_object_src && |
| 269 UpdateStatus(directive_name, &tokenizer, &object_src_status, options, |
| 270 &sane_csp_parts, warnings)) |
| 257 continue; | 271 continue; |
| 258 if (ParsePluginTypes(directive_name, tokenizer, &plugin_types)) | 272 |
| 259 continue; | 273 // Pass the other CSP directives as-is without further validation. |
| 274 sane_csp_parts.push_back(input + ";"); |
| 260 } | 275 } |
| 261 | 276 |
| 262 if (script_src_status.seen_in_policy && !script_src_status.is_secure) | 277 if (default_src_status.seen_in_policy) { |
| 263 return false; | 278 if (!script_src_status.seen_in_policy || |
| 264 | 279 !object_src_status.seen_in_policy) { |
| 265 if (object_src_status.seen_in_policy && !object_src_status.is_secure) { | 280 // Insecure values in default-src are only relevant if either script-src |
| 266 // Note that this does not fully check the object-src source list for | 281 // or object-src is omitted. |
| 267 // validity but Blink will do this anyway. | 282 if (warnings) |
| 268 if (!AllowedToHaveInsecureObjectSrc(options, plugin_types)) | 283 warnings->insert(warnings->end(), |
| 269 return false; | 284 default_src_csp_warnings.begin(), |
| 285 default_src_csp_warnings.end()); |
| 286 } |
| 287 } else { |
| 288 if (!script_src_status.seen_in_policy) { |
| 289 sane_csp_parts.push_back(kScriptSrcDefaultDirective); |
| 290 if (warnings) |
| 291 warnings->push_back(CSPInstallWarning(ErrorUtils::FormatErrorMessage( |
| 292 manifest_errors::kInvalidCSPMissingSecureSrc, kScriptSrc))); |
| 293 } |
| 294 if (!object_src_status.seen_in_policy && !allow_insecure_object_src) { |
| 295 sane_csp_parts.push_back(kObjectSrcDefaultDirective); |
| 296 if (warnings) |
| 297 warnings->push_back(CSPInstallWarning(ErrorUtils::FormatErrorMessage( |
| 298 manifest_errors::kInvalidCSPMissingSecureSrc, kObjectSrc))); |
| 299 } |
| 270 } | 300 } |
| 271 | 301 |
| 272 if (default_src_status.seen_in_policy && !default_src_status.is_secure) { | 302 return JoinString(sane_csp_parts, ' '); |
| 273 return script_src_status.seen_in_policy && | |
| 274 object_src_status.seen_in_policy; | |
| 275 } | |
| 276 | |
| 277 return default_src_status.seen_in_policy || | |
| 278 (script_src_status.seen_in_policy && object_src_status.seen_in_policy); | |
| 279 } | 303 } |
| 280 | 304 |
| 281 bool ContentSecurityPolicyIsSandboxed( | 305 bool ContentSecurityPolicyIsSandboxed( |
| 282 const std::string& policy, Manifest::Type type) { | 306 const std::string& policy, Manifest::Type type) { |
| 283 // See http://www.w3.org/TR/CSP/#parse-a-csp-policy for parsing algorithm. | 307 // See http://www.w3.org/TR/CSP/#parse-a-csp-policy for parsing algorithm. |
| 284 std::vector<std::string> directives; | 308 std::vector<std::string> directives; |
| 285 base::SplitString(policy, ';', &directives); | 309 base::SplitString(policy, ';', &directives); |
| 286 | 310 |
| 287 bool seen_sandbox = false; | 311 bool seen_sandbox = false; |
| 288 | 312 |
| (...skipping 26 matching lines...) Expand all Loading... |
| 315 } | 339 } |
| 316 } | 340 } |
| 317 } | 341 } |
| 318 | 342 |
| 319 return seen_sandbox; | 343 return seen_sandbox; |
| 320 } | 344 } |
| 321 | 345 |
| 322 } // namespace csp_validator | 346 } // namespace csp_validator |
| 323 | 347 |
| 324 } // namespace extensions | 348 } // namespace extensions |
| OLD | NEW |