Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / extensions / common / csp_validator.cc
blob8f9406fe196d97f42ca8fcb8615cf1de8f485849
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
3 // found in the LICENSE file.
5 #include "extensions/common/csp_validator.h"
7 #include <vector>
9 #include "base/strings/string_split.h"
10 #include "base/strings/string_tokenizer.h"
11 #include "base/strings/string_util.h"
12 #include "content/public/common/url_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"
17 #include "net/base/registry_controlled_domains/registry_controlled_domain.h"
19 namespace extensions {
21 namespace csp_validator {
23 namespace {
25 const char kDefaultSrc[] = "default-src";
26 const char kScriptSrc[] = "script-src";
27 const char kObjectSrc[] = "object-src";
28 const char kPluginTypes[] = "plugin-types";
30 const char kObjectSrcDefaultDirective[] = "object-src 'self';";
31 const char kScriptSrcDefaultDirective[] =
32 "script-src 'self' chrome-extension-resource:;";
34 const char kSandboxDirectiveName[] = "sandbox";
35 const char kAllowSameOriginToken[] = "allow-same-origin";
36 const char kAllowTopNavigation[] = "allow-top-navigation";
38 // This is the list of plugin types which are fully sandboxed and are safe to
39 // load up in an extension, regardless of the URL they are navigated to.
40 const char* const kSandboxedPluginTypes[] = {
41 "application/pdf",
42 "application/x-google-chrome-pdf",
43 "application/x-pnacl"
46 // List of CSP hash-source prefixes that are accepted. Blink is a bit more
47 // lenient, but we only accept standard hashes to be forward-compatible.
48 // http://www.w3.org/TR/2015/CR-CSP2-20150721/#hash_algo
49 const char* const kHashSourcePrefixes[] = {
50 "'sha256-",
51 "'sha384-",
52 "'sha512-"
55 struct DirectiveStatus {
56 explicit DirectiveStatus(const char* name)
57 : directive_name(name), seen_in_policy(false) {}
59 const char* directive_name;
60 bool seen_in_policy;
63 // Returns whether |url| starts with |scheme_and_separator| and does not have a
64 // too permissive wildcard host name. If |should_check_rcd| is true, then the
65 // Public suffix list is used to exclude wildcard TLDs such as "https://*.org".
66 bool isNonWildcardTLD(const std::string& url,
67 const std::string& scheme_and_separator,
68 bool should_check_rcd) {
69 if (!base::StartsWith(url, scheme_and_separator,
70 base::CompareCase::SENSITIVE))
71 return false;
73 size_t start_of_host = scheme_and_separator.length();
75 size_t end_of_host = url.find("/", start_of_host);
76 if (end_of_host == std::string::npos)
77 end_of_host = url.size();
79 // Note: It is sufficient to only compare the first character against '*'
80 // because the CSP only allows wildcards at the start of a directive, see
81 // host-source and host-part at http://www.w3.org/TR/CSP2/#source-list-syntax
82 bool is_wildcard_subdomain = end_of_host > start_of_host + 2 &&
83 url[start_of_host] == '*' && url[start_of_host + 1] == '.';
84 if (is_wildcard_subdomain)
85 start_of_host += 2;
87 size_t start_of_port = url.rfind(":", end_of_host);
88 // The ":" check at the end of the following condition is used to avoid
89 // treating the last part of an IPv6 address as a port.
90 if (start_of_port > start_of_host && url[start_of_port - 1] != ':') {
91 bool is_valid_port = false;
92 // Do a quick sanity check. The following check could mistakenly flag
93 // ":123456" or ":****" as valid, but that does not matter because the
94 // relaxing CSP directive will just be ignored by Blink.
95 for (size_t i = start_of_port + 1; i < end_of_host; ++i) {
96 is_valid_port = base::IsAsciiDigit(url[i]) || url[i] == '*';
97 if (!is_valid_port)
98 break;
100 if (is_valid_port)
101 end_of_host = start_of_port;
104 std::string host(url, start_of_host, end_of_host - start_of_host);
105 // Global wildcards are not allowed.
106 if (host.empty() || host.find("*") != std::string::npos)
107 return false;
109 if (!is_wildcard_subdomain || !should_check_rcd)
110 return true;
112 // Allow *.googleapis.com to be whitelisted for backwards-compatibility.
113 // (crbug.com/409952)
114 if (host == "googleapis.com")
115 return true;
117 // Wildcards on subdomains of a TLD are not allowed.
118 size_t registry_length = net::registry_controlled_domains::GetRegistryLength(
119 host,
120 net::registry_controlled_domains::INCLUDE_UNKNOWN_REGISTRIES,
121 net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
122 return registry_length != 0;
125 // Checks whether the source is a syntactically valid hash.
126 bool IsHashSource(const std::string& source) {
127 size_t hash_end = source.length() - 1;
128 if (source.empty() || source[hash_end] != '\'') {
129 return false;
132 for (const char* prefix : kHashSourcePrefixes) {
133 if (base::StartsWith(source, prefix,
134 base::CompareCase::INSENSITIVE_ASCII)) {
135 for (size_t i = strlen(prefix); i < hash_end; ++i) {
136 const char c = source[i];
137 // The hash must be base64-encoded. Do not allow any other characters.
138 if (!base::IsAsciiAlpha(c) && !base::IsAsciiDigit(c) && c != '+' &&
139 c != '/' && c != '=') {
140 return false;
143 return true;
146 return false;
149 InstallWarning CSPInstallWarning(const std::string& csp_warning) {
150 return InstallWarning(csp_warning, manifest_keys::kContentSecurityPolicy);
153 void GetSecureDirectiveValues(const std::string& directive_name,
154 base::StringTokenizer* tokenizer,
155 int options,
156 std::vector<std::string>* sane_csp_parts,
157 std::vector<InstallWarning>* warnings) {
158 sane_csp_parts->push_back(directive_name);
159 while (tokenizer->GetNext()) {
160 std::string source_literal = tokenizer->token();
161 std::string source_lower = base::ToLowerASCII(source_literal);
162 bool is_secure_csp_token = false;
164 // We might need to relax this whitelist over time.
165 if (source_lower == "'self'" || source_lower == "'none'" ||
166 source_lower == "http://127.0.0.1" || source_lower == "blob:" ||
167 source_lower == "filesystem:" || source_lower == "http://localhost" ||
168 base::StartsWith(source_lower, "http://127.0.0.1:",
169 base::CompareCase::SENSITIVE) ||
170 base::StartsWith(source_lower, "http://localhost:",
171 base::CompareCase::SENSITIVE) ||
172 isNonWildcardTLD(source_lower, "https://", true) ||
173 isNonWildcardTLD(source_lower, "chrome://", false) ||
174 isNonWildcardTLD(source_lower,
175 std::string(extensions::kExtensionScheme) +
176 url::kStandardSchemeSeparator,
177 false) ||
178 IsHashSource(source_literal) ||
179 base::StartsWith(source_lower, "chrome-extension-resource:",
180 base::CompareCase::SENSITIVE)) {
181 is_secure_csp_token = true;
182 } else if ((options & OPTIONS_ALLOW_UNSAFE_EVAL) &&
183 source_lower == "'unsafe-eval'") {
184 is_secure_csp_token = true;
187 if (is_secure_csp_token) {
188 sane_csp_parts->push_back(source_literal);
189 } else if (warnings) {
190 warnings->push_back(CSPInstallWarning(ErrorUtils::FormatErrorMessage(
191 manifest_errors::kInvalidCSPInsecureValue, source_literal,
192 directive_name)));
195 // End of CSP directive that was started at the beginning of this method. If
196 // none of the values are secure, the policy will be empty and default to
197 // 'none', which is secure.
198 sane_csp_parts->back().push_back(';');
201 // Returns true if |directive_name| matches |status.directive_name|.
202 bool UpdateStatus(const std::string& directive_name,
203 base::StringTokenizer* tokenizer,
204 DirectiveStatus* status,
205 int options,
206 std::vector<std::string>* sane_csp_parts,
207 std::vector<InstallWarning>* warnings) {
208 if (directive_name != status->directive_name)
209 return false;
211 if (!status->seen_in_policy) {
212 status->seen_in_policy = true;
213 GetSecureDirectiveValues(directive_name, tokenizer, options, sane_csp_parts,
214 warnings);
215 } else {
216 // Don't show any errors for duplicate CSP directives, because it will be
217 // ignored by the CSP parser (http://www.w3.org/TR/CSP2/#policy-parsing).
218 GetSecureDirectiveValues(directive_name, tokenizer, options, sane_csp_parts,
219 NULL);
221 return true;
224 // Returns true if the |plugin_type| is one of the fully sandboxed plugin types.
225 bool PluginTypeAllowed(const std::string& plugin_type) {
226 for (size_t i = 0; i < arraysize(kSandboxedPluginTypes); ++i) {
227 if (plugin_type == kSandboxedPluginTypes[i])
228 return true;
230 return false;
233 // Returns true if the policy is allowed to contain an insecure object-src
234 // directive. This requires OPTIONS_ALLOW_INSECURE_OBJECT_SRC to be specified
235 // as an option and the plugin-types that can be loaded must be restricted to
236 // the set specified in kSandboxedPluginTypes.
237 bool AllowedToHaveInsecureObjectSrc(
238 int options,
239 const std::vector<std::string>& directives) {
240 if (!(options & OPTIONS_ALLOW_INSECURE_OBJECT_SRC))
241 return false;
243 for (size_t i = 0; i < directives.size(); ++i) {
244 const std::string& input = directives[i];
245 base::StringTokenizer tokenizer(input, " \t\r\n");
246 if (!tokenizer.GetNext())
247 continue;
248 if (!base::LowerCaseEqualsASCII(tokenizer.token(), kPluginTypes))
249 continue;
250 while (tokenizer.GetNext()) {
251 if (!PluginTypeAllowed(tokenizer.token()))
252 return false;
254 // All listed plugin types are whitelisted.
255 return true;
257 // plugin-types not specified.
258 return false;
261 } // namespace
263 bool ContentSecurityPolicyIsLegal(const std::string& policy) {
264 // We block these characters to prevent HTTP header injection when
265 // representing the content security policy as an HTTP header.
266 const char kBadChars[] = {',', '\r', '\n', '\0'};
268 return policy.find_first_of(kBadChars, 0, arraysize(kBadChars)) ==
269 std::string::npos;
272 std::string SanitizeContentSecurityPolicy(
273 const std::string& policy,
274 int options,
275 std::vector<InstallWarning>* warnings) {
276 // See http://www.w3.org/TR/CSP/#parse-a-csp-policy for parsing algorithm.
277 std::vector<std::string> directives = base::SplitString(
278 policy, ";", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
280 DirectiveStatus default_src_status(kDefaultSrc);
281 DirectiveStatus script_src_status(kScriptSrc);
282 DirectiveStatus object_src_status(kObjectSrc);
284 bool allow_insecure_object_src =
285 AllowedToHaveInsecureObjectSrc(options, directives);
287 std::vector<std::string> sane_csp_parts;
288 std::vector<InstallWarning> default_src_csp_warnings;
289 for (size_t i = 0; i < directives.size(); ++i) {
290 std::string& input = directives[i];
291 base::StringTokenizer tokenizer(input, " \t\r\n");
292 if (!tokenizer.GetNext())
293 continue;
295 std::string directive_name = base::ToLowerASCII(tokenizer.token_piece());
296 if (UpdateStatus(directive_name, &tokenizer, &default_src_status, options,
297 &sane_csp_parts, &default_src_csp_warnings))
298 continue;
299 if (UpdateStatus(directive_name, &tokenizer, &script_src_status, options,
300 &sane_csp_parts, warnings))
301 continue;
302 if (!allow_insecure_object_src &&
303 UpdateStatus(directive_name, &tokenizer, &object_src_status, options,
304 &sane_csp_parts, warnings))
305 continue;
307 // Pass the other CSP directives as-is without further validation.
308 sane_csp_parts.push_back(input + ";");
311 if (default_src_status.seen_in_policy) {
312 if (!script_src_status.seen_in_policy ||
313 !object_src_status.seen_in_policy) {
314 // Insecure values in default-src are only relevant if either script-src
315 // or object-src is omitted.
316 if (warnings)
317 warnings->insert(warnings->end(),
318 default_src_csp_warnings.begin(),
319 default_src_csp_warnings.end());
321 } else {
322 if (!script_src_status.seen_in_policy) {
323 sane_csp_parts.push_back(kScriptSrcDefaultDirective);
324 if (warnings)
325 warnings->push_back(CSPInstallWarning(ErrorUtils::FormatErrorMessage(
326 manifest_errors::kInvalidCSPMissingSecureSrc, kScriptSrc)));
328 if (!object_src_status.seen_in_policy && !allow_insecure_object_src) {
329 sane_csp_parts.push_back(kObjectSrcDefaultDirective);
330 if (warnings)
331 warnings->push_back(CSPInstallWarning(ErrorUtils::FormatErrorMessage(
332 manifest_errors::kInvalidCSPMissingSecureSrc, kObjectSrc)));
336 return base::JoinString(sane_csp_parts, " ");
339 bool ContentSecurityPolicyIsSandboxed(
340 const std::string& policy, Manifest::Type type) {
341 // See http://www.w3.org/TR/CSP/#parse-a-csp-policy for parsing algorithm.
342 bool seen_sandbox = false;
343 for (const std::string& input : base::SplitString(
344 policy, ";", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL)) {
345 base::StringTokenizer tokenizer(input, " \t\r\n");
346 if (!tokenizer.GetNext())
347 continue;
349 std::string directive_name = base::ToLowerASCII(tokenizer.token_piece());
350 if (directive_name != kSandboxDirectiveName)
351 continue;
353 seen_sandbox = true;
355 while (tokenizer.GetNext()) {
356 std::string token = base::ToLowerASCII(tokenizer.token_piece());
358 // The same origin token negates the sandboxing.
359 if (token == kAllowSameOriginToken)
360 return false;
362 // Platform apps don't allow navigation.
363 if (type == Manifest::TYPE_PLATFORM_APP) {
364 if (token == kAllowTopNavigation)
365 return false;
370 return seen_sandbox;
373 } // namespace csp_validator
375 } // namespace extensions