Chromium Code Reviews| 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"; |
| 28 const char kObjectSrcDefaultDirective[] = "object-src 'self';"; | |
| 29 const char kScriptSrcDefaultDirective[] = | |
| 30 "script-src 'self' chrome-extension-resource:;"; | |
| 25 | 31 |
| 26 const char kSandboxDirectiveName[] = "sandbox"; | 32 const char kSandboxDirectiveName[] = "sandbox"; |
| 27 const char kAllowSameOriginToken[] = "allow-same-origin"; | 33 const char kAllowSameOriginToken[] = "allow-same-origin"; |
| 28 const char kAllowTopNavigation[] = "allow-top-navigation"; | 34 const char kAllowTopNavigation[] = "allow-top-navigation"; |
| 29 | 35 |
| 30 struct DirectiveStatus { | 36 struct DirectiveStatus { |
| 31 explicit DirectiveStatus(const char* name) | 37 explicit DirectiveStatus(const char* name) |
| 32 : directive_name(name) | 38 : directive_name(name), seen_in_policy(false) {} |
| 33 , seen_in_policy(false) | |
| 34 , is_secure(false) { | |
| 35 } | |
| 36 | 39 |
| 37 const char* directive_name; | 40 const char* directive_name; |
| 38 bool seen_in_policy; | 41 bool seen_in_policy; |
| 39 bool is_secure; | |
| 40 }; | 42 }; |
| 41 | 43 |
| 42 // Returns whether |url| starts with |scheme_and_separator| and does not have a | 44 // Returns whether |url| starts with |scheme_and_separator| and does not have a |
| 43 // too permissive wildcard host name. If |should_check_rcd| is true, then the | 45 // too permissive wildcard host name. If |should_check_rcd| is true, then the |
| 44 // Public suffix list is used to exclude wildcard TLDs such as "https://*.org". | 46 // Public suffix list is used to exclude wildcard TLDs such as "https://*.org". |
| 45 bool isNonWildcardTLD(const std::string& url, | 47 bool isNonWildcardTLD(const std::string& url, |
| 46 const std::string& scheme_and_separator, | 48 const std::string& scheme_and_separator, |
| 47 bool should_check_rcd) { | 49 bool should_check_rcd) { |
| 48 if (!StartsWithASCII(url, scheme_and_separator, true)) | 50 if (!StartsWithASCII(url, scheme_and_separator, true)) |
| 49 return false; | 51 return false; |
| 50 | 52 |
| 51 size_t start_of_host = scheme_and_separator.length(); | 53 size_t start_of_host = scheme_and_separator.length(); |
| 52 | 54 |
| 53 size_t end_of_host = url.find("/", start_of_host); | 55 size_t end_of_host = url.find("/", start_of_host); |
| 54 if (end_of_host == std::string::npos) | 56 if (end_of_host == std::string::npos) |
| 55 end_of_host = url.size(); | 57 end_of_host = url.size(); |
| 56 | 58 |
| 57 // A missing host such as "chrome-extension://" is invalid, but for backwards- | |
| 58 // compatibility, accept such CSP parts. They will be ignored by Blink anyway. | |
| 59 // TODO(robwu): Remove this special case once crbug.com/434773 is fixed. | |
| 60 if (start_of_host == end_of_host) | |
| 61 return true; | |
| 62 | |
| 63 // Note: It is sufficient to only compare the first character against '*' | 59 // Note: It is sufficient to only compare the first character against '*' |
| 64 // because the CSP only allows wildcards at the start of a directive, see | 60 // because the CSP only allows wildcards at the start of a directive, see |
| 65 // host-source and host-part at http://www.w3.org/TR/CSP2/#source-list-syntax | 61 // host-source and host-part at http://www.w3.org/TR/CSP2/#source-list-syntax |
| 66 bool is_wildcard_subdomain = end_of_host > start_of_host + 2 && | 62 bool is_wildcard_subdomain = end_of_host > start_of_host + 2 && |
| 67 url[start_of_host] == '*' && url[start_of_host + 1] == '.'; | 63 url[start_of_host] == '*' && url[start_of_host + 1] == '.'; |
| 68 if (is_wildcard_subdomain) | 64 if (is_wildcard_subdomain) |
| 69 start_of_host += 2; | 65 start_of_host += 2; |
| 70 | 66 |
| 71 size_t start_of_port = url.rfind(":", end_of_host); | 67 size_t start_of_port = url.rfind(":", end_of_host); |
| 72 // The ":" check at the end of the following condition is used to avoid | 68 // The ":" check at the end of the following condition is used to avoid |
| (...skipping 26 matching lines...) Expand all Loading... | |
| 99 return true; | 95 return true; |
| 100 | 96 |
| 101 // Wildcards on subdomains of a TLD are not allowed. | 97 // Wildcards on subdomains of a TLD are not allowed. |
| 102 size_t registry_length = net::registry_controlled_domains::GetRegistryLength( | 98 size_t registry_length = net::registry_controlled_domains::GetRegistryLength( |
| 103 host, | 99 host, |
| 104 net::registry_controlled_domains::INCLUDE_UNKNOWN_REGISTRIES, | 100 net::registry_controlled_domains::INCLUDE_UNKNOWN_REGISTRIES, |
| 105 net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES); | 101 net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES); |
| 106 return registry_length != 0; | 102 return registry_length != 0; |
| 107 } | 103 } |
| 108 | 104 |
| 109 bool HasOnlySecureTokens(base::StringTokenizer& tokenizer, | 105 void GetSecureDirectiveValues(const std::string& directive_name, |
| 110 int options) { | 106 base::StringTokenizer& tokenizer, |
| 107 int options, | |
| 108 std::vector<std::string>& csp_parts, | |
| 109 std::vector<std::string>* csp_warnings) { | |
| 110 csp_parts.push_back(directive_name); | |
|
not at google - send to devlin
2014/12/01 19:19:31
Only pass const references. If you need to modify
robwu
2014/12/02 23:42:09
Done.
| |
| 111 while (tokenizer.GetNext()) { | 111 while (tokenizer.GetNext()) { |
| 112 std::string source = tokenizer.token(); | 112 std::string source = tokenizer.token(); |
| 113 base::StringToLowerASCII(&source); | 113 base::StringToLowerASCII(&source); |
| 114 | 114 |
| 115 // We might need to relax this whitelist over time. | 115 // We might need to relax this whitelist over time. |
| 116 if (source == "'self'" || | 116 if (source == "'self'" || |
| 117 source == "'none'" || | 117 source == "'none'" || |
| 118 source == "http://127.0.0.1" || | 118 source == "http://127.0.0.1" || |
| 119 LowerCaseEqualsASCII(source, "blob:") || | 119 LowerCaseEqualsASCII(source, "blob:") || |
| 120 LowerCaseEqualsASCII(source, "filesystem:") || | 120 LowerCaseEqualsASCII(source, "filesystem:") || |
| 121 LowerCaseEqualsASCII(source, "http://localhost") || | 121 LowerCaseEqualsASCII(source, "http://localhost") || |
| 122 StartsWithASCII(source, "http://127.0.0.1:", true) || | 122 StartsWithASCII(source, "http://127.0.0.1:", true) || |
| 123 StartsWithASCII(source, "http://localhost:", true) || | 123 StartsWithASCII(source, "http://localhost:", true) || |
| 124 isNonWildcardTLD(source, "https://", true) || | 124 isNonWildcardTLD(source, "https://", true) || |
| 125 isNonWildcardTLD(source, "chrome://", false) || | 125 isNonWildcardTLD(source, "chrome://", false) || |
| 126 isNonWildcardTLD(source, | 126 isNonWildcardTLD(source, |
| 127 std::string(extensions::kExtensionScheme) + | 127 std::string(extensions::kExtensionScheme) + |
| 128 url::kStandardSchemeSeparator, | 128 url::kStandardSchemeSeparator, |
| 129 false) || | 129 false) || |
| 130 StartsWithASCII(source, "chrome-extension-resource:", true)) { | 130 StartsWithASCII(source, "chrome-extension-resource:", true)) { |
| 131 csp_parts.push_back(source); | |
| 131 continue; | 132 continue; |
| 132 } | 133 } |
| 133 | 134 |
| 134 if (options & OPTIONS_ALLOW_UNSAFE_EVAL) { | 135 if (options & OPTIONS_ALLOW_UNSAFE_EVAL) { |
| 135 if (source == "'unsafe-eval'") | 136 if (source == "'unsafe-eval'") { |
| 137 csp_parts.push_back(source); | |
| 136 continue; | 138 continue; |
| 139 } | |
| 137 } | 140 } |
|
not at google - send to devlin
2014/12/01 19:19:31
I think this would be slightly easier to read if y
robwu
2014/12/02 23:42:09
Done.
| |
| 138 | 141 if (csp_warnings) { |
| 139 return false; | 142 csp_warnings->push_back(ErrorUtils::FormatErrorMessage( |
| 143 manifest_errors::kInvalidCSPInsecureValue, source, directive_name)); | |
| 144 } | |
| 140 } | 145 } |
| 141 | 146 // End of CSP directive that was started at the beginning of this method. If |
| 142 return true; // Empty values default to 'none', which is secure. | 147 // none of the values are secure, the policy will be empty and default to |
| 148 // 'none', which is secure. | |
| 149 csp_parts.back().push_back(';'); | |
| 143 } | 150 } |
| 144 | 151 |
| 145 // Returns true if |directive_name| matches |status.directive_name|. | 152 // Returns true if |directive_name| matches |status.directive_name|. |
| 146 bool UpdateStatus(const std::string& directive_name, | 153 bool UpdateStatus(const std::string& directive_name, |
| 147 base::StringTokenizer& tokenizer, | 154 base::StringTokenizer& tokenizer, |
| 148 DirectiveStatus* status, | 155 DirectiveStatus* status, |
| 149 int options) { | 156 int options, |
| 150 if (status->seen_in_policy) | 157 std::vector<std::string>& csp_parts, |
|
not at google - send to devlin
2014/12/01 19:19:31
Same comment re reference vs pointer.
robwu
2014/12/02 23:42:08
Done.
| |
| 151 return false; | 158 std::vector<std::string>* csp_warnings) { |
| 152 if (directive_name != status->directive_name) | 159 if (directive_name != status->directive_name) |
| 153 return false; | 160 return false; |
| 154 status->seen_in_policy = true; | 161 |
| 155 status->is_secure = HasOnlySecureTokens(tokenizer, options); | 162 if (!status->seen_in_policy) { |
| 163 status->seen_in_policy = true; | |
| 164 GetSecureDirectiveValues(directive_name, tokenizer, options, csp_parts, | |
| 165 csp_warnings); | |
| 166 } else { | |
| 167 // Don't show any errors for duplicate CSP directives, because it will be | |
| 168 // ignored by the CSP parser (http://www.w3.org/TR/CSP2/#policy-parsing). | |
| 169 GetSecureDirectiveValues(directive_name, tokenizer, options, csp_parts, | |
| 170 NULL); | |
| 171 } | |
| 156 return true; | 172 return true; |
| 157 } | 173 } |
| 158 | 174 |
| 159 } // namespace | 175 } // namespace |
| 160 | 176 |
| 161 bool ContentSecurityPolicyIsLegal(const std::string& policy) { | 177 bool ContentSecurityPolicyIsLegal(const std::string& policy) { |
| 162 // We block these characters to prevent HTTP header injection when | 178 // We block these characters to prevent HTTP header injection when |
| 163 // representing the content security policy as an HTTP header. | 179 // representing the content security policy as an HTTP header. |
| 164 const char kBadChars[] = {',', '\r', '\n', '\0'}; | 180 const char kBadChars[] = {',', '\r', '\n', '\0'}; |
| 165 | 181 |
| 166 return policy.find_first_of(kBadChars, 0, arraysize(kBadChars)) == | 182 return policy.find_first_of(kBadChars, 0, arraysize(kBadChars)) == |
| 167 std::string::npos; | 183 std::string::npos; |
| 168 } | 184 } |
| 169 | 185 |
| 170 bool ContentSecurityPolicyIsSecure(const std::string& policy, | 186 bool ContentSecurityPolicyIsSecure(const std::string& policy, |
| 171 int options) { | 187 int options, |
| 188 std::string* sanitized_csp, | |
|
not at google - send to devlin
2014/12/01 19:19:31
I'm worried that the way the code has been modifie
robwu
2014/12/02 23:42:08
SanitizeContentSecurityPolicy sgtm. I'll do that i
| |
| 189 std::vector<InstallWarning>* warnings) { | |
| 172 // See http://www.w3.org/TR/CSP/#parse-a-csp-policy for parsing algorithm. | 190 // See http://www.w3.org/TR/CSP/#parse-a-csp-policy for parsing algorithm. |
| 173 std::vector<std::string> directives; | 191 std::vector<std::string> directives; |
| 174 base::SplitString(policy, ';', &directives); | 192 base::SplitString(policy, ';', &directives); |
| 175 | 193 |
| 176 DirectiveStatus default_src_status(kDefaultSrc); | 194 DirectiveStatus default_src_status(kDefaultSrc); |
| 177 DirectiveStatus script_src_status(kScriptSrc); | 195 DirectiveStatus script_src_status(kScriptSrc); |
| 178 DirectiveStatus object_src_status(kObjectSrc); | 196 DirectiveStatus object_src_status(kObjectSrc); |
| 179 | 197 |
| 198 std::vector<std::string> csp_parts; | |
|
not at google - send to devlin
2014/12/01 19:19:31
sane_csp? (basically what this is generating).
robwu
2014/12/02 23:42:09
Done. (sane_csp_parts)
| |
| 199 std::vector<std::string> csp_warnings; | |
| 200 std::vector<std::string> default_src_csp_warnings; | |
|
not at google - send to devlin
2014/12/01 19:19:31
Can you just directly pass around |warnings|?
robwu
2014/12/02 23:42:08
Instead of csp_warnings? Yes, especially after I d
not at google - send to devlin
2014/12/03 20:33:05
Yes that is what I meant; don't even declare a sep
| |
| 180 for (size_t i = 0; i < directives.size(); ++i) { | 201 for (size_t i = 0; i < directives.size(); ++i) { |
| 181 std::string& input = directives[i]; | 202 std::string& input = directives[i]; |
| 182 base::StringTokenizer tokenizer(input, " \t\r\n"); | 203 base::StringTokenizer tokenizer(input, " \t\r\n"); |
| 183 if (!tokenizer.GetNext()) | 204 if (!tokenizer.GetNext()) |
| 184 continue; | 205 continue; |
| 185 | 206 |
| 186 std::string directive_name = tokenizer.token(); | 207 std::string directive_name = tokenizer.token(); |
| 187 base::StringToLowerASCII(&directive_name); | 208 base::StringToLowerASCII(&directive_name); |
| 188 | 209 |
| 189 if (UpdateStatus(directive_name, tokenizer, &default_src_status, options)) | 210 if (UpdateStatus(directive_name, tokenizer, &default_src_status, options, |
| 211 csp_parts, &default_src_csp_warnings)) | |
| 190 continue; | 212 continue; |
| 191 if (UpdateStatus(directive_name, tokenizer, &script_src_status, options)) | 213 if (UpdateStatus(directive_name, tokenizer, &script_src_status, options, |
| 214 csp_parts, &csp_warnings)) | |
| 192 continue; | 215 continue; |
| 193 if (UpdateStatus(directive_name, tokenizer, &object_src_status, options)) | 216 if (!(options & OPTIONS_ALLOW_INSECURE_OBJECT_SRC) && |
| 217 UpdateStatus(directive_name, tokenizer, &object_src_status, options, | |
| 218 csp_parts, &csp_warnings)) | |
| 194 continue; | 219 continue; |
| 220 | |
| 221 // Pass the other CSP directives as-is without further validation. | |
| 222 csp_parts.push_back(input + ";"); | |
| 195 } | 223 } |
| 196 | 224 |
| 197 if (script_src_status.seen_in_policy && !script_src_status.is_secure) | 225 if (default_src_status.seen_in_policy) { |
| 198 return false; | 226 if (!script_src_status.seen_in_policy || |
| 199 | 227 !object_src_status.seen_in_policy) { |
| 200 if (object_src_status.seen_in_policy && !object_src_status.is_secure) { | 228 // Insecure values in default-src are only relevant if either script-src |
| 201 // Note that this does not fully check the object-src source list for | 229 // or object-src is omitted. |
| 202 // validity but Blink will do this anyway. | 230 csp_warnings.insert(csp_warnings.end(), |
| 203 if (!(options & OPTIONS_ALLOW_INSECURE_OBJECT_SRC)) | 231 default_src_csp_warnings.begin(), |
| 204 return false; | 232 default_src_csp_warnings.end()); |
| 233 } | |
| 234 } else { | |
| 235 if (!script_src_status.seen_in_policy) { | |
| 236 csp_parts.push_back(kScriptSrcDefaultDirective); | |
| 237 csp_warnings.push_back(ErrorUtils::FormatErrorMessage( | |
| 238 manifest_errors::kInvalidCSPMissingSecureSrc, kScriptSrc)); | |
| 239 } | |
| 240 if (!object_src_status.seen_in_policy && | |
| 241 !(options & OPTIONS_ALLOW_INSECURE_OBJECT_SRC)) { | |
| 242 csp_parts.push_back(kObjectSrcDefaultDirective); | |
| 243 csp_warnings.push_back(ErrorUtils::FormatErrorMessage( | |
| 244 manifest_errors::kInvalidCSPMissingSecureSrc, kObjectSrc)); | |
| 245 } | |
| 205 } | 246 } |
| 206 | 247 |
| 207 if (default_src_status.seen_in_policy && !default_src_status.is_secure) { | 248 if (sanitized_csp) |
| 208 return script_src_status.seen_in_policy && | 249 *sanitized_csp = JoinString(csp_parts, ' '); |
| 209 object_src_status.seen_in_policy; | 250 if (csp_warnings.empty()) |
| 251 return true; | |
| 252 | |
| 253 if (warnings) { | |
| 254 for (auto csp_warning : csp_warnings) { | |
|
not at google - send to devlin
2014/12/01 19:19:31
To avoid string copy I think you need const auto&
robwu
2014/12/02 23:42:08
Done.
| |
| 255 warnings->push_back( | |
| 256 InstallWarning(csp_warning, manifest_keys::kContentSecurityPolicy)); | |
| 257 } | |
| 210 } | 258 } |
| 211 | 259 return false; |
| 212 return default_src_status.seen_in_policy || | |
| 213 (script_src_status.seen_in_policy && object_src_status.seen_in_policy); | |
| 214 } | 260 } |
| 215 | 261 |
| 216 bool ContentSecurityPolicyIsSandboxed( | 262 bool ContentSecurityPolicyIsSandboxed( |
| 217 const std::string& policy, Manifest::Type type) { | 263 const std::string& policy, Manifest::Type type) { |
| 218 // See http://www.w3.org/TR/CSP/#parse-a-csp-policy for parsing algorithm. | 264 // See http://www.w3.org/TR/CSP/#parse-a-csp-policy for parsing algorithm. |
| 219 std::vector<std::string> directives; | 265 std::vector<std::string> directives; |
| 220 base::SplitString(policy, ';', &directives); | 266 base::SplitString(policy, ';', &directives); |
| 221 | 267 |
| 222 bool seen_sandbox = false; | 268 bool seen_sandbox = false; |
| 223 | 269 |
| (...skipping 26 matching lines...) Expand all Loading... | |
| 250 } | 296 } |
| 251 } | 297 } |
| 252 } | 298 } |
| 253 | 299 |
| 254 return seen_sandbox; | 300 return seen_sandbox; |
| 255 } | 301 } |
| 256 | 302 |
| 257 } // namespace csp_validator | 303 } // namespace csp_validator |
| 258 | 304 |
| 259 } // namespace extensions | 305 } // namespace extensions |
| OLD | NEW |