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

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

Issue 747403002: Ignore insecure parts of CSP in extensions and allow extension to load (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: rebase Created 6 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 <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
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
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
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698