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 Manifest::Type type) { | 106 base::StringTokenizer& tokenizer, |
107 Manifest::Type type, | |
108 std::vector<std::string>& csp_parts, | |
109 std::vector<std::string>* csp_warnings) { | |
110 csp_parts.push_back(directive_name); | |
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 // crbug.com/146487 | 135 // crbug.com/146487 |
135 if (type == Manifest::TYPE_EXTENSION || | 136 if (type == Manifest::TYPE_EXTENSION || |
136 type == Manifest::TYPE_LEGACY_PACKAGED_APP) { | 137 type == Manifest::TYPE_LEGACY_PACKAGED_APP) { |
137 if (source == "'unsafe-eval'") | 138 if (source == "'unsafe-eval'") { |
139 csp_parts.push_back(source); | |
138 continue; | 140 continue; |
141 } | |
139 } | 142 } |
140 | 143 if (csp_warnings) { |
141 return false; | 144 csp_warnings->push_back(ErrorUtils::FormatErrorMessage( |
145 manifest_errors::kInvalidCSPInsecureValue, source, directive_name)); | |
146 } | |
142 } | 147 } |
143 | 148 // End of CSP directive that was started at the beginning of this method. If |
144 return true; // Empty values default to 'none', which is secure. | 149 // none of the values are secure, the policy will be empty and default to |
150 // 'none', which is secure. | |
151 csp_parts.back() += ";"; | |
Mike West
2014/11/24 11:37:19
Assigning to the result of a method call is iffy.
robwu
2014/11/24 12:19:47
Done.
| |
145 } | 152 } |
146 | 153 |
147 // Returns true if |directive_name| matches |status.directive_name|. | 154 // Returns true if |directive_name| matches |status.directive_name|. |
148 bool UpdateStatus(const std::string& directive_name, | 155 bool UpdateStatus(const std::string& directive_name, |
149 base::StringTokenizer& tokenizer, | 156 base::StringTokenizer& tokenizer, |
150 DirectiveStatus* status, | 157 DirectiveStatus* status, |
151 Manifest::Type type) { | 158 Manifest::Type type, |
152 if (status->seen_in_policy) | 159 std::vector<std::string>& csp_parts, |
153 return false; | 160 std::vector<std::string>* csp_warnings) { |
154 if (directive_name != status->directive_name) | 161 if (directive_name != status->directive_name) |
155 return false; | 162 return false; |
156 status->seen_in_policy = true; | 163 |
157 status->is_secure = HasOnlySecureTokens(tokenizer, type); | 164 if (!status->seen_in_policy) { |
165 status->seen_in_policy = true; | |
166 GetSecureDirectiveValues(directive_name, tokenizer, type, csp_parts, | |
167 csp_warnings); | |
168 } else { | |
169 // Don't show any errors for duplicate CSP directives, because it will be | |
170 // ignored by the CSP parser (http://www.w3.org/TR/CSP2/#policy-parsing). | |
171 GetSecureDirectiveValues(directive_name, tokenizer, type, csp_parts, NULL); | |
172 } | |
158 return true; | 173 return true; |
159 } | 174 } |
160 | 175 |
161 } // namespace | 176 } // namespace |
162 | 177 |
163 bool ContentSecurityPolicyIsLegal(const std::string& policy) { | 178 bool ContentSecurityPolicyIsLegal(const std::string& policy) { |
164 // We block these characters to prevent HTTP header injection when | 179 // We block these characters to prevent HTTP header injection when |
165 // representing the content security policy as an HTTP header. | 180 // representing the content security policy as an HTTP header. |
166 const char kBadChars[] = {',', '\r', '\n', '\0'}; | 181 const char kBadChars[] = {',', '\r', '\n', '\0'}; |
167 | 182 |
168 return policy.find_first_of(kBadChars, 0, arraysize(kBadChars)) == | 183 return policy.find_first_of(kBadChars, 0, arraysize(kBadChars)) == |
169 std::string::npos; | 184 std::string::npos; |
170 } | 185 } |
171 | 186 |
172 bool ContentSecurityPolicyIsSecure(const std::string& policy, | 187 bool ContentSecurityPolicyIsSecure(const std::string& policy, |
173 Manifest::Type type) { | 188 Manifest::Type type, |
189 std::string* sanitized_csp, | |
190 std::vector<InstallWarning>* warnings) { | |
174 // See http://www.w3.org/TR/CSP/#parse-a-csp-policy for parsing algorithm. | 191 // See http://www.w3.org/TR/CSP/#parse-a-csp-policy for parsing algorithm. |
175 std::vector<std::string> directives; | 192 std::vector<std::string> directives; |
176 base::SplitString(policy, ';', &directives); | 193 base::SplitString(policy, ';', &directives); |
177 | 194 |
178 DirectiveStatus default_src_status(kDefaultSrc); | 195 DirectiveStatus default_src_status(kDefaultSrc); |
179 DirectiveStatus script_src_status(kScriptSrc); | 196 DirectiveStatus script_src_status(kScriptSrc); |
180 DirectiveStatus object_src_status(kObjectSrc); | 197 DirectiveStatus object_src_status(kObjectSrc); |
181 | 198 |
199 std::vector<std::string> csp_parts; | |
200 std::vector<std::string> csp_warnings; | |
201 std::vector<std::string> default_src_csp_warnings; | |
182 for (size_t i = 0; i < directives.size(); ++i) { | 202 for (size_t i = 0; i < directives.size(); ++i) { |
183 std::string& input = directives[i]; | 203 std::string& input = directives[i]; |
184 base::StringTokenizer tokenizer(input, " \t\r\n"); | 204 base::StringTokenizer tokenizer(input, " \t\r\n"); |
185 if (!tokenizer.GetNext()) | 205 if (!tokenizer.GetNext()) |
186 continue; | 206 continue; |
187 | 207 |
188 std::string directive_name = tokenizer.token(); | 208 std::string directive_name = tokenizer.token(); |
189 base::StringToLowerASCII(&directive_name); | 209 base::StringToLowerASCII(&directive_name); |
190 | 210 |
191 if (UpdateStatus(directive_name, tokenizer, &default_src_status, type)) | 211 if (UpdateStatus(directive_name, tokenizer, &default_src_status, type, |
212 csp_parts, &default_src_csp_warnings)) | |
192 continue; | 213 continue; |
193 if (UpdateStatus(directive_name, tokenizer, &script_src_status, type)) | 214 if (UpdateStatus(directive_name, tokenizer, &script_src_status, type, |
215 csp_parts, &csp_warnings)) | |
194 continue; | 216 continue; |
195 if (UpdateStatus(directive_name, tokenizer, &object_src_status, type)) | 217 if (UpdateStatus(directive_name, tokenizer, &object_src_status, type, |
218 csp_parts, &csp_warnings)) | |
196 continue; | 219 continue; |
220 | |
221 // Pass the other CSP directives as-is without further validation. | |
222 csp_parts.push_back(input + ";"); | |
197 } | 223 } |
198 | 224 |
199 if (script_src_status.seen_in_policy && !script_src_status.is_secure) | 225 if (default_src_status.seen_in_policy) { |
200 return false; | 226 if (!script_src_status.seen_in_policy || |
201 | 227 !object_src_status.seen_in_policy) { |
202 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 |
203 return false; | 229 // or object-src is omitted. |
204 | 230 std::move(default_src_csp_warnings.begin(), |
Mike West
2014/11/24 11:37:19
We can't use `std::move` or `std::back_inserter` y
robwu
2014/11/24 12:19:47
Done. Replaced with insert.
| |
205 if (default_src_status.seen_in_policy && !default_src_status.is_secure) { | 231 default_src_csp_warnings.end(), |
206 return script_src_status.seen_in_policy && | 232 std::back_inserter(csp_warnings)); |
Mike West
2014/11/24 11:37:19
Don't you also need to inject the default `script-
robwu
2014/11/24 12:19:47
No, they are inferred from default-src.
| |
207 object_src_status.seen_in_policy; | 233 } |
Mike West
2014/11/24 11:37:19
I don't see whether you're verifying that the `def
robwu
2014/11/24 12:19:46
GetSecureDirectiveValues (via UpdateStatus) ensure
| |
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 csp_parts.push_back(kObjectSrcDefaultDirective); | |
242 csp_warnings.push_back(ErrorUtils::FormatErrorMessage( | |
243 manifest_errors::kInvalidCSPMissingSecureSrc, kObjectSrc)); | |
244 } | |
208 } | 245 } |
209 | 246 |
210 return default_src_status.seen_in_policy || | 247 if (sanitized_csp) |
211 (script_src_status.seen_in_policy && object_src_status.seen_in_policy); | 248 *sanitized_csp = JoinString(csp_parts, ' '); |
249 if (csp_warnings.empty()) | |
250 return true; | |
251 | |
252 if (warnings) { | |
253 for (std::vector<std::string>::iterator it = csp_warnings.begin(); | |
Mike West
2014/11/24 11:37:19
You can use a C++11 for loop and 'auto' here for c
robwu
2014/11/24 12:19:47
Done.
| |
254 it != csp_warnings.end(); ++it) { | |
255 warnings->push_back( | |
256 InstallWarning(*it, manifest_keys::kContentSecurityPolicy)); | |
257 } | |
258 } | |
259 return false; | |
212 } | 260 } |
213 | 261 |
214 bool ContentSecurityPolicyIsSandboxed( | 262 bool ContentSecurityPolicyIsSandboxed( |
215 const std::string& policy, Manifest::Type type) { | 263 const std::string& policy, Manifest::Type type) { |
216 // 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. |
217 std::vector<std::string> directives; | 265 std::vector<std::string> directives; |
218 base::SplitString(policy, ';', &directives); | 266 base::SplitString(policy, ';', &directives); |
219 | 267 |
220 bool seen_sandbox = false; | 268 bool seen_sandbox = false; |
221 | 269 |
(...skipping 26 matching lines...) Expand all Loading... | |
248 } | 296 } |
249 } | 297 } |
250 } | 298 } |
251 | 299 |
252 return seen_sandbox; | 300 return seen_sandbox; |
253 } | 301 } |
254 | 302 |
255 } // namespace csp_validator | 303 } // namespace csp_validator |
256 | 304 |
257 } // namespace extensions | 305 } // namespace extensions |
OLD | NEW |