[go: nahoru, domu]

blob: b54e1b7cb9d680e86a2a8480835374e8096385af [file] [log] [blame]
rdevlin.cronin@chromium.org70c39bb2013-11-26 22:59:281// Copyright 2013 The Chromium Authors. All rights reserved.
abarth@chromium.orgee0be772011-12-02 08:02:102// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
rdevlin.cronin@chromium.org70c39bb2013-11-26 22:59:285#include "extensions/common/csp_validator.h"
abarth@chromium.orgee0be772011-12-02 08:02:106
avi2d124c02015-12-23 06:36:427#include <stddef.h>
8
Karan Bhatiad9937722018-10-24 02:40:159#include <algorithm>
lazyboyc0304f62017-01-03 21:17:3010#include <initializer_list>
Karan Bhatia37927c82018-10-09 23:21:2611#include <iterator>
Karan Bhatia11c67e592019-01-24 04:28:0012#include <set>
Karan Bhatiaf34832e82019-01-18 02:02:0013#include <utility>
haibane84@gmail.comb1248eb2014-04-16 07:13:1314#include <vector>
15
lazyboyc0304f62017-01-03 21:17:3016#include "base/bind.h"
17#include "base/callback.h"
Hans Wennborg09979592020-04-27 12:34:3018#include "base/check_op.h"
Avi Drissman197295ae2018-12-25 20:28:3419#include "base/stl_util.h"
mgiuca30f75882017-03-28 02:07:1920#include "base/strings/string_piece.h"
tfarina@chromium.org1988e1c2013-02-28 20:27:4221#include "base/strings/string_split.h"
avi@chromium.orgd2e754f2013-06-11 01:41:4722#include "base/strings/string_util.h"
lazyboyc0304f62017-01-03 21:17:3023#include "base/strings/stringprintf.h"
ivandavid@chromium.org99bee642014-05-31 22:36:4324#include "content/public/common/url_constants.h"
25#include "extensions/common/constants.h"
robf19335612015-01-07 00:38:3526#include "extensions/common/error_utils.h"
27#include "extensions/common/install_warning.h"
28#include "extensions/common/manifest_constants.h"
rob@robwu.nl30f0f602014-08-19 23:48:2229#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
abarth@chromium.orgee0be772011-12-02 08:02:1030
31namespace extensions {
32
33namespace csp_validator {
34
35namespace {
36
37const char kDefaultSrc[] = "default-src";
38const char kScriptSrc[] = "script-src";
39const char kObjectSrc[] = "object-src";
lazyboyc0304f62017-01-03 21:17:3040const char kFrameSrc[] = "frame-src";
41const char kChildSrc[] = "child-src";
Karan Bhatia11c67e592019-01-24 04:28:0042const char kWorkerSrc[] = "worker-src";
43const char kSelfSource[] = "'self'";
44const char kNoneSource[] = "'none'";
lazyboyc0304f62017-01-03 21:17:3045
46const char kDirectiveSeparator = ';';
47
robf19335612015-01-07 00:38:3548const char kObjectSrcDefaultDirective[] = "object-src 'self';";
nick0fbc3922016-12-16 20:52:0749const char kScriptSrcDefaultDirective[] = "script-src 'self';";
robf19335612015-01-07 00:38:3550
lazyboyc0304f62017-01-03 21:17:3051const char kAppSandboxSubframeSrcDefaultDirective[] = "child-src 'self';";
52const char kAppSandboxScriptSrcDefaultDirective[] =
53 "script-src 'self' 'unsafe-inline' 'unsafe-eval';";
54
mihaip@chromium.orgdbb24162012-06-06 01:41:2255const char kSandboxDirectiveName[] = "sandbox";
56const char kAllowSameOriginToken[] = "allow-same-origin";
57const char kAllowTopNavigation[] = "allow-top-navigation";
mihaip@chromium.orgdbb24162012-06-06 01:41:2258
rob8131e672015-08-10 23:36:4559// List of CSP hash-source prefixes that are accepted. Blink is a bit more
60// lenient, but we only accept standard hashes to be forward-compatible.
61// http://www.w3.org/TR/2015/CR-CSP2-20150721/#hash_algo
62const char* const kHashSourcePrefixes[] = {
63 "'sha256-",
64 "'sha384-",
65 "'sha512-"
66};
67
Karan Bhatiad9937722018-10-24 02:40:1568// TODO(karandeepb): This is not the same list as used by the CSP spec. See
69// https://infra.spec.whatwg.org/#ascii-whitespace.
70const char kWhitespaceDelimiters[] = " \t\r\n";
71
72using Directive = CSPParser::Directive;
73
74// TODO(karandeepb): Rename this to DirectiveSet (as used in spec, see
75// https://www.w3.org/TR/CSP/#policy-directive-set) once we ensure that this
76// does not contain any duplicates.
77using DirectiveList = CSPParser::DirectiveList;
78
Karan Bhatia11c67e592019-01-24 04:28:0079bool IsLocalHostSource(const std::string& source_lower) {
80 DCHECK_EQ(base::ToLowerASCII(source_lower), source_lower);
81
82 constexpr char kLocalHost[] = "http://localhost";
83 constexpr char kLocalHostIP[] = "http://127.0.0.1";
84
85 // Subtracting 1 to exclude the null terminator '\0'.
86 constexpr size_t kLocalHostLen = base::size(kLocalHost) - 1;
87 constexpr size_t kLocalHostIPLen = base::size(kLocalHostIP) - 1;
88
89 if (base::StartsWith(source_lower, kLocalHost,
90 base::CompareCase::SENSITIVE)) {
91 return source_lower.length() == kLocalHostLen ||
92 source_lower[kLocalHostLen] == ':';
93 }
94
95 if (base::StartsWith(source_lower, kLocalHostIP,
96 base::CompareCase::SENSITIVE)) {
97 return source_lower.length() == kLocalHostIPLen ||
98 source_lower[kLocalHostIPLen] == ':';
99 }
100
101 return false;
102}
103
lazyboyc0304f62017-01-03 21:17:30104// Represents the status of a directive in a CSP string.
105//
106// Examples of directive:
107// script source related: scrict-src
108// subframe source related: child-src/frame-src.
109class DirectiveStatus {
110 public:
111 // Subframe related directives can have multiple directive names: "child-src"
112 // or "frame-src".
113 DirectiveStatus(std::initializer_list<const char*> directives)
114 : directive_names_(directives.begin(), directives.end()) {}
abarth@chromium.orgee0be772011-12-02 08:02:10115
Karan Bhatia11c67e592019-01-24 04:28:00116 DirectiveStatus(DirectiveStatus&&) = default;
117 DirectiveStatus& operator=(DirectiveStatus&&) = default;
118
lazyboyc0304f62017-01-03 21:17:30119 // Returns true if |directive_name| matches this DirectiveStatus.
120 bool Matches(const std::string& directive_name) const {
121 for (const auto& directive : directive_names_) {
122 if (!base::CompareCaseInsensitiveASCII(directive_name, directive))
123 return true;
124 }
125 return false;
126 }
127
128 bool seen_in_policy() const { return seen_in_policy_; }
129 void set_seen_in_policy() { seen_in_policy_ = true; }
130
131 const std::string& name() const {
132 DCHECK(!directive_names_.empty());
133 return directive_names_[0];
134 }
135
136 private:
137 // The CSP directive names this DirectiveStatus cares about.
138 std::vector<std::string> directive_names_;
139 // Whether or not we've seen any directive name that matches |this|.
140 bool seen_in_policy_ = false;
141
142 DISALLOW_COPY_AND_ASSIGN(DirectiveStatus);
abarth@chromium.orgee0be772011-12-02 08:02:10143};
144
rob@robwu.nl30f0f602014-08-19 23:48:22145// Returns whether |url| starts with |scheme_and_separator| and does not have a
146// too permissive wildcard host name. If |should_check_rcd| is true, then the
147// Public suffix list is used to exclude wildcard TLDs such as "https://*.org".
148bool isNonWildcardTLD(const std::string& url,
149 const std::string& scheme_and_separator,
150 bool should_check_rcd) {
brettw95509312015-07-16 23:57:33151 if (!base::StartsWith(url, scheme_and_separator,
152 base::CompareCase::SENSITIVE))
rob@robwu.nl30f0f602014-08-19 23:48:22153 return false;
154
155 size_t start_of_host = scheme_and_separator.length();
156
157 size_t end_of_host = url.find("/", start_of_host);
158 if (end_of_host == std::string::npos)
159 end_of_host = url.size();
160
161 // Note: It is sufficient to only compare the first character against '*'
162 // because the CSP only allows wildcards at the start of a directive, see
163 // host-source and host-part at http://www.w3.org/TR/CSP2/#source-list-syntax
164 bool is_wildcard_subdomain = end_of_host > start_of_host + 2 &&
165 url[start_of_host] == '*' && url[start_of_host + 1] == '.';
166 if (is_wildcard_subdomain)
167 start_of_host += 2;
168
169 size_t start_of_port = url.rfind(":", end_of_host);
170 // The ":" check at the end of the following condition is used to avoid
171 // treating the last part of an IPv6 address as a port.
172 if (start_of_port > start_of_host && url[start_of_port - 1] != ':') {
173 bool is_valid_port = false;
174 // Do a quick sanity check. The following check could mistakenly flag
175 // ":123456" or ":****" as valid, but that does not matter because the
176 // relaxing CSP directive will just be ignored by Blink.
177 for (size_t i = start_of_port + 1; i < end_of_host; ++i) {
brettwb3413062015-06-24 00:39:02178 is_valid_port = base::IsAsciiDigit(url[i]) || url[i] == '*';
rob@robwu.nl30f0f602014-08-19 23:48:22179 if (!is_valid_port)
180 break;
181 }
182 if (is_valid_port)
183 end_of_host = start_of_port;
184 }
185
186 std::string host(url, start_of_host, end_of_host - start_of_host);
187 // Global wildcards are not allowed.
188 if (host.empty() || host.find("*") != std::string::npos)
189 return false;
190
191 if (!is_wildcard_subdomain || !should_check_rcd)
192 return true;
193
rob00c9de32014-10-25 09:42:33194 // Allow *.googleapis.com to be whitelisted for backwards-compatibility.
195 // (crbug.com/409952)
196 if (host == "googleapis.com")
197 return true;
198
rob@robwu.nl30f0f602014-08-19 23:48:22199 // Wildcards on subdomains of a TLD are not allowed.
brettw5a36380ef2016-10-27 19:51:56200 return net::registry_controlled_domains::HostHasRegistryControlledDomain(
201 host, net::registry_controlled_domains::INCLUDE_UNKNOWN_REGISTRIES,
rob@robwu.nl30f0f602014-08-19 23:48:22202 net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
rob@robwu.nl30f0f602014-08-19 23:48:22203}
204
rob8131e672015-08-10 23:36:45205// Checks whether the source is a syntactically valid hash.
lazyboyc0304f62017-01-03 21:17:30206bool IsHashSource(base::StringPiece source) {
207 if (source.empty() || source.back() != '\'')
rob8131e672015-08-10 23:36:45208 return false;
rob8131e672015-08-10 23:36:45209
lazyboyc0304f62017-01-03 21:17:30210 size_t hash_end = source.length() - 1;
rob8131e672015-08-10 23:36:45211 for (const char* prefix : kHashSourcePrefixes) {
212 if (base::StartsWith(source, prefix,
213 base::CompareCase::INSENSITIVE_ASCII)) {
214 for (size_t i = strlen(prefix); i < hash_end; ++i) {
215 const char c = source[i];
216 // The hash must be base64-encoded. Do not allow any other characters.
217 if (!base::IsAsciiAlpha(c) && !base::IsAsciiDigit(c) && c != '+' &&
218 c != '/' && c != '=') {
219 return false;
220 }
221 }
222 return true;
223 }
224 }
225 return false;
226}
227
lazyboyc0304f62017-01-03 21:17:30228std::string GetSecureDirectiveValues(
229 int options,
230 const std::string& directive_name,
231 const std::vector<base::StringPiece>& directive_values,
Karan Bhatiaf34832e82019-01-18 02:02:00232 const std::string& manifest_key,
lazyboyc0304f62017-01-03 21:17:30233 std::vector<InstallWarning>* warnings) {
mgiuca30f75882017-03-28 02:07:19234 std::vector<base::StringPiece> sane_csp_parts{directive_name};
lazyboyc0304f62017-01-03 21:17:30235 for (base::StringPiece source_literal : directive_values) {
rob8131e672015-08-10 23:36:45236 std::string source_lower = base::ToLowerASCII(source_literal);
robf19335612015-01-07 00:38:35237 bool is_secure_csp_token = false;
abarth@chromium.orgee0be772011-12-02 08:02:10238
abarth@chromium.orgee0be772011-12-02 08:02:10239 // We might need to relax this whitelist over time.
Karan Bhatia11c67e592019-01-24 04:28:00240 if (source_lower == kSelfSource || source_lower == kNoneSource ||
241 source_lower == "'wasm-eval'" || source_lower == "blob:" ||
242 source_lower == "filesystem:" ||
rob8131e672015-08-10 23:36:45243 isNonWildcardTLD(source_lower, "https://", true) ||
244 isNonWildcardTLD(source_lower, "chrome://", false) ||
245 isNonWildcardTLD(source_lower,
246 std::string(extensions::kExtensionScheme) +
247 url::kStandardSchemeSeparator,
rob@robwu.nl30f0f602014-08-19 23:48:22248 false) ||
Karan Bhatia11c67e592019-01-24 04:28:00249 IsHashSource(source_literal) || IsLocalHostSource(source_lower)) {
robf19335612015-01-07 00:38:35250 is_secure_csp_token = true;
251 } else if ((options & OPTIONS_ALLOW_UNSAFE_EVAL) &&
rob8131e672015-08-10 23:36:45252 source_lower == "'unsafe-eval'") {
robf19335612015-01-07 00:38:35253 is_secure_csp_token = true;
abarth@chromium.orgee0be772011-12-02 08:02:10254 }
255
robf19335612015-01-07 00:38:35256 if (is_secure_csp_token) {
mgiuca30f75882017-03-28 02:07:19257 sane_csp_parts.push_back(source_literal);
robf19335612015-01-07 00:38:35258 } else if (warnings) {
Karan Bhatiaf34832e82019-01-18 02:02:00259 warnings->push_back(InstallWarning(
260 ErrorUtils::FormatErrorMessage(
Karan Bhatia11c67e592019-01-24 04:28:00261 manifest_errors::kInvalidCSPInsecureValueIgnored, manifest_key,
Karan Bhatiaf34832e82019-01-18 02:02:00262 source_literal.as_string(), directive_name),
263 manifest_key));
aa@chromium.org65474d912012-09-07 01:02:51264 }
abarth@chromium.orgee0be772011-12-02 08:02:10265 }
robf19335612015-01-07 00:38:35266 // End of CSP directive that was started at the beginning of this method. If
267 // none of the values are secure, the policy will be empty and default to
268 // 'none', which is secure.
mgiuca30f75882017-03-28 02:07:19269 std::string last_part = sane_csp_parts.back().as_string();
270 last_part.push_back(kDirectiveSeparator);
271 sane_csp_parts.back() = last_part;
lazyboyc0304f62017-01-03 21:17:30272 return base::JoinString(sane_csp_parts, " ");
abarth@chromium.orgee0be772011-12-02 08:02:10273}
274
lazyboyc0304f62017-01-03 21:17:30275// Given a CSP directive-token for app sandbox, returns a secure value of that
276// directive.
277// The directive-token's name is |directive_name| and its values are splitted
278// into |directive_values|.
279std::string GetAppSandboxSecureDirectiveValues(
280 const std::string& directive_name,
281 const std::vector<base::StringPiece>& directive_values,
Karan Bhatiaf34832e82019-01-18 02:02:00282 const std::string& manifest_key,
lazyboyc0304f62017-01-03 21:17:30283 std::vector<InstallWarning>* warnings) {
mgiuca30f75882017-03-28 02:07:19284 std::vector<std::string> sane_csp_parts{directive_name};
lazyboyc0304f62017-01-03 21:17:30285 bool seen_self_or_none = false;
286 for (base::StringPiece source_literal : directive_values) {
287 std::string source_lower = base::ToLowerASCII(source_literal);
abarth@chromium.orgee0be772011-12-02 08:02:10288
lazyboyc0304f62017-01-03 21:17:30289 // Keyword directive sources are surrounded with quotes, e.g. 'self',
290 // 'sha256-...', 'unsafe-eval', 'nonce-...'. These do not specify a remote
291 // host or '*', so keep them and restrict the rest.
292 if (source_lower.size() > 1u && source_lower[0] == '\'' &&
293 source_lower.back() == '\'') {
294 seen_self_or_none |= source_lower == "'none'" || source_lower == "'self'";
295 sane_csp_parts.push_back(source_lower);
296 } else if (warnings) {
Karan Bhatiaf34832e82019-01-18 02:02:00297 warnings->push_back(InstallWarning(
298 ErrorUtils::FormatErrorMessage(
Karan Bhatia11c67e592019-01-24 04:28:00299 manifest_errors::kInvalidCSPInsecureValueIgnored, manifest_key,
Karan Bhatiaf34832e82019-01-18 02:02:00300 source_literal.as_string(), directive_name),
301 manifest_key));
lazyboyc0304f62017-01-03 21:17:30302 }
raymes3c02e4d2014-11-26 02:27:56303 }
lazyboyc0304f62017-01-03 21:17:30304
305 // If we haven't seen any of 'self' or 'none', that means this directive
306 // value isn't secure. Specify 'self' to secure it.
307 if (!seen_self_or_none)
308 sane_csp_parts.push_back("'self'");
309
310 sane_csp_parts.back().push_back(kDirectiveSeparator);
311 return base::JoinString(sane_csp_parts, " ");
raymes3c02e4d2014-11-26 02:27:56312}
313
Alan Cutter04a00642020-03-02 01:45:20314using SecureDirectiveValueFunction = base::RepeatingCallback<std::string(
lazyboyc0304f62017-01-03 21:17:30315 const std::string& directive_name,
316 const std::vector<base::StringPiece>& directive_values,
Karan Bhatiaf34832e82019-01-18 02:02:00317 const std::string& manifest_key,
lazyboyc0304f62017-01-03 21:17:30318 std::vector<InstallWarning>* warnings)>;
319
320// Represents a token in CSP string.
321// Tokens are delimited by ";" CSP string.
322class CSPDirectiveToken {
323 public:
Karan Bhatiad9937722018-10-24 02:40:15324 // We hold a reference to |directive|, so it must remain alive longer than
325 // |this|.
326 explicit CSPDirectiveToken(const Directive& directive)
327 : directive_(directive) {}
lazyboyc0304f62017-01-03 21:17:30328
329 // Returns true if this token affects |status|. In that case, the token's
330 // directive values are secured by |secure_function|.
331 bool MatchAndUpdateStatus(DirectiveStatus* status,
332 const SecureDirectiveValueFunction& secure_function,
Karan Bhatiaf34832e82019-01-18 02:02:00333 const std::string& manifest_key,
lazyboyc0304f62017-01-03 21:17:30334 std::vector<InstallWarning>* warnings) {
Karan Bhatiad9937722018-10-24 02:40:15335 if (!status->Matches(directive_.directive_name))
lazyboyc0304f62017-01-03 21:17:30336 return false;
337
lazyboyc0304f62017-01-03 21:17:30338 bool is_duplicate_directive = status->seen_in_policy();
339 status->set_seen_in_policy();
340
341 secure_value_ = secure_function.Run(
Karan Bhatiaf34832e82019-01-18 02:02:00342 directive_.directive_name, directive_.directive_values, manifest_key,
lazyboyc0304f62017-01-03 21:17:30343 // Don't show any errors for duplicate CSP directives, because it will
344 // be ignored by the CSP parser
345 // (http://www.w3.org/TR/CSP2/#policy-parsing). Therefore, set warnings
346 // param to nullptr.
347 is_duplicate_directive ? nullptr : warnings);
348 return true;
349 }
350
351 std::string ToString() {
352 if (secure_value_)
353 return secure_value_.value();
354 // This token didn't require modification.
Karan Bhatiad9937722018-10-24 02:40:15355 return directive_.directive_string.as_string() + kDirectiveSeparator;
lazyboyc0304f62017-01-03 21:17:30356 }
357
358 private:
Karan Bhatiad9937722018-10-24 02:40:15359 const Directive& directive_;
lazyboyc0304f62017-01-03 21:17:30360 base::Optional<std::string> secure_value_;
361
lazyboyc0304f62017-01-03 21:17:30362 DISALLOW_COPY_AND_ASSIGN(CSPDirectiveToken);
363};
364
365// Class responsible for parsing a given CSP string |policy|, and enforcing
366// secure directive-tokens within the policy.
367//
368// If a CSP directive's value is not secure, this class will use secure
369// values (via |secure_function|). If a CSP directive-token is not present and
370// as a result will fallback to default (possibly non-secure), this class
371// will use default secure values (via GetDefaultCSPValue).
372class CSPEnforcer {
373 public:
Karan Bhatiaf34832e82019-01-18 02:02:00374 CSPEnforcer(std::string manifest_key,
375 bool show_missing_csp_warnings,
lazyboyc0304f62017-01-03 21:17:30376 const SecureDirectiveValueFunction& secure_function)
Karan Bhatiaf34832e82019-01-18 02:02:00377 : manifest_key_(std::move(manifest_key)),
378 show_missing_csp_warnings_(show_missing_csp_warnings),
lazyboyc0304f62017-01-03 21:17:30379 secure_function_(secure_function) {}
380 virtual ~CSPEnforcer() {}
381
382 // Returns the enforced CSP.
383 // Emits warnings in |warnings| for insecure directive values. If
384 // |show_missing_csp_warnings_| is true, these will also include missing CSP
385 // directive warnings.
Karan Bhatiad9937722018-10-24 02:40:15386 std::string Enforce(const DirectiveList& directives,
lazyboyc0304f62017-01-03 21:17:30387 std::vector<InstallWarning>* warnings);
388
389 protected:
390 virtual std::string GetDefaultCSPValue(const DirectiveStatus& status) = 0;
391
392 // List of directives we care about.
Karan Bhatia11c67e592019-01-24 04:28:00393 // TODO(karandeepb): There is no reason for these to be on the heap. Stack
394 // allocate.
Karan Bhatiad9937722018-10-24 02:40:15395 std::vector<std::unique_ptr<DirectiveStatus>> secure_directives_;
lazyboyc0304f62017-01-03 21:17:30396
397 private:
Karan Bhatiaf34832e82019-01-18 02:02:00398 const std::string manifest_key_;
lazyboyc0304f62017-01-03 21:17:30399 const bool show_missing_csp_warnings_;
400 const SecureDirectiveValueFunction secure_function_;
401
402 DISALLOW_COPY_AND_ASSIGN(CSPEnforcer);
403};
404
Karan Bhatiad9937722018-10-24 02:40:15405std::string CSPEnforcer::Enforce(const DirectiveList& directives,
lazyboyc0304f62017-01-03 21:17:30406 std::vector<InstallWarning>* warnings) {
Karan Bhatiad9937722018-10-24 02:40:15407 DCHECK(!secure_directives_.empty());
lazyboyc0304f62017-01-03 21:17:30408 std::vector<std::string> enforced_csp_parts;
409
410 // If any directive that we care about isn't explicitly listed in |policy|,
411 // "default-src" fallback is used.
412 DirectiveStatus default_src_status({kDefaultSrc});
413 std::vector<InstallWarning> default_src_csp_warnings;
414
Karan Bhatiad9937722018-10-24 02:40:15415 for (const auto& directive : directives) {
416 CSPDirectiveToken csp_directive_token(directive);
lazyboyc0304f62017-01-03 21:17:30417 bool matches_enforcing_directive = false;
Karan Bhatiad9937722018-10-24 02:40:15418 for (const std::unique_ptr<DirectiveStatus>& status : secure_directives_) {
lazyboyc0304f62017-01-03 21:17:30419 if (csp_directive_token.MatchAndUpdateStatus(
Karan Bhatiaf34832e82019-01-18 02:02:00420 status.get(), secure_function_, manifest_key_, warnings)) {
lazyboyc0304f62017-01-03 21:17:30421 matches_enforcing_directive = true;
422 break;
423 }
424 }
425 if (!matches_enforcing_directive) {
Karan Bhatiaf34832e82019-01-18 02:02:00426 csp_directive_token.MatchAndUpdateStatus(&default_src_status,
427 secure_function_, manifest_key_,
428 &default_src_csp_warnings);
lazyboyc0304f62017-01-03 21:17:30429 }
430
431 enforced_csp_parts.push_back(csp_directive_token.ToString());
432 }
433
434 if (default_src_status.seen_in_policy()) {
Karan Bhatiad9937722018-10-24 02:40:15435 for (const std::unique_ptr<DirectiveStatus>& status : secure_directives_) {
lazyboyc0304f62017-01-03 21:17:30436 if (!status->seen_in_policy()) {
437 // This |status| falls back to "default-src". So warnings from
438 // "default-src" will apply.
439 if (warnings) {
Karan Bhatia37927c82018-10-09 23:21:26440 warnings->insert(
441 warnings->end(),
442 std::make_move_iterator(default_src_csp_warnings.begin()),
443 std::make_move_iterator(default_src_csp_warnings.end()));
lazyboyc0304f62017-01-03 21:17:30444 }
445 break;
446 }
447 }
448 } else {
449 // Did not see "default-src".
Karan Bhatiad9937722018-10-24 02:40:15450 // Make sure we cover all sources from |secure_directives_|.
451 for (const std::unique_ptr<DirectiveStatus>& status : secure_directives_) {
lazyboyc0304f62017-01-03 21:17:30452 if (status->seen_in_policy()) // Already covered.
453 continue;
454 enforced_csp_parts.push_back(GetDefaultCSPValue(*status));
455
456 if (warnings && show_missing_csp_warnings_) {
Karan Bhatiaf34832e82019-01-18 02:02:00457 warnings->push_back(
458 InstallWarning(ErrorUtils::FormatErrorMessage(
459 manifest_errors::kInvalidCSPMissingSecureSrc,
460 manifest_key_, status->name()),
461 manifest_key_));
lazyboyc0304f62017-01-03 21:17:30462 }
463 }
464 }
465
466 return base::JoinString(enforced_csp_parts, " ");
467}
468
469class ExtensionCSPEnforcer : public CSPEnforcer {
470 public:
Karan Bhatiaf34832e82019-01-18 02:02:00471 ExtensionCSPEnforcer(std::string manifest_key,
472 bool allow_insecure_object_src,
473 int options)
474 : CSPEnforcer(std::move(manifest_key),
475 true,
Alexander Cooper0d265c62021-01-30 01:24:22476 base::BindRepeating(&GetSecureDirectiveValues, options)) {
Karan Bhatiad9937722018-10-24 02:40:15477 secure_directives_.emplace_back(new DirectiveStatus({kScriptSrc}));
lazyboyc0304f62017-01-03 21:17:30478 if (!allow_insecure_object_src)
Karan Bhatiad9937722018-10-24 02:40:15479 secure_directives_.emplace_back(new DirectiveStatus({kObjectSrc}));
lazyboyc0304f62017-01-03 21:17:30480 }
481
482 protected:
483 std::string GetDefaultCSPValue(const DirectiveStatus& status) override {
484 if (status.Matches(kObjectSrc))
485 return kObjectSrcDefaultDirective;
486 DCHECK(status.Matches(kScriptSrc));
487 return kScriptSrcDefaultDirective;
488 }
489
490 private:
491 DISALLOW_COPY_AND_ASSIGN(ExtensionCSPEnforcer);
492};
493
494class AppSandboxPageCSPEnforcer : public CSPEnforcer {
495 public:
Karan Bhatiaf34832e82019-01-18 02:02:00496 AppSandboxPageCSPEnforcer(std::string manifest_key)
497 : CSPEnforcer(std::move(manifest_key),
498 false,
Alexander Cooper0d265c62021-01-30 01:24:22499 base::BindRepeating(&GetAppSandboxSecureDirectiveValues)) {
Karan Bhatiad9937722018-10-24 02:40:15500 secure_directives_.emplace_back(
501 new DirectiveStatus({kChildSrc, kFrameSrc}));
502 secure_directives_.emplace_back(new DirectiveStatus({kScriptSrc}));
lazyboyc0304f62017-01-03 21:17:30503 }
504
505 protected:
506 std::string GetDefaultCSPValue(const DirectiveStatus& status) override {
507 if (status.Matches(kChildSrc))
508 return kAppSandboxSubframeSrcDefaultDirective;
509 DCHECK(status.Matches(kScriptSrc));
510 return kAppSandboxScriptSrcDefaultDirective;
511 }
512
513 private:
514 DISALLOW_COPY_AND_ASSIGN(AppSandboxPageCSPEnforcer);
515};
516
abarth@chromium.orgee0be772011-12-02 08:02:10517} // namespace
518
519bool ContentSecurityPolicyIsLegal(const std::string& policy) {
520 // We block these characters to prevent HTTP header injection when
521 // representing the content security policy as an HTTP header.
mkwst@chromium.org8a510ac2012-09-06 02:23:08522 const char kBadChars[] = {',', '\r', '\n', '\0'};
abarth@chromium.orgee0be772011-12-02 08:02:10523
Avi Drissman197295ae2018-12-25 20:28:34524 return policy.find_first_of(kBadChars, 0, base::size(kBadChars)) ==
lazyboyc0304f62017-01-03 21:17:30525 std::string::npos;
abarth@chromium.orgee0be772011-12-02 08:02:10526}
527
Karan Bhatiad9937722018-10-24 02:40:15528Directive::Directive(base::StringPiece directive_string,
529 std::string directive_name,
530 std::vector<base::StringPiece> directive_values)
531 : directive_string(directive_string),
532 directive_name(std::move(directive_name)),
533 directive_values(std::move(directive_values)) {
534 // |directive_name| should be lower cased.
535 DCHECK(std::none_of(directive_name.begin(), directive_name.end(),
536 base::IsAsciiUpper<char>));
537}
538
539CSPParser::Directive::~Directive() = default;
540CSPParser::Directive::Directive(Directive&&) = default;
541CSPParser::Directive& Directive::operator=(Directive&&) = default;
542
543CSPParser::CSPParser(std::string policy) : policy_(std::move(policy)) {
544 Parse();
545}
546CSPParser::~CSPParser() = default;
547
548void CSPParser::Parse() {
549 // See http://www.w3.org/TR/CSP/#parse-a-csp-policy for parsing algorithm.
550 for (const base::StringPiece directive_str : base::SplitStringPiece(
551 policy_, ";", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY)) {
552 // Get whitespace separated tokens.
553 std::vector<base::StringPiece> tokens = base::SplitStringPiece(
554 directive_str, kWhitespaceDelimiters, base::TRIM_WHITESPACE,
555 base::SPLIT_WANT_NONEMPTY);
556
557 // |directive_str| is non-empty and has had whitespace trimmed. Hence, it
558 // must contain some non-whitespace characters.
559 DCHECK(!tokens.empty());
560
561 // TODO(karandeepb): As per http://www.w3.org/TR/CSP/#parse-a-csp-policy,
562 // we should ignore duplicate directive names. We should raise an install
563 // warning for them.
564 std::string directive_name = base::ToLowerASCII(tokens[0]);
565
566 // Erase the first token, the directive name.
567 tokens.erase(tokens.begin());
568
569 directives_.emplace_back(directive_str, std::move(directive_name),
570 std::move(tokens));
571 }
572}
573
robf19335612015-01-07 00:38:35574std::string SanitizeContentSecurityPolicy(
575 const std::string& policy,
Karan Bhatiaf34832e82019-01-18 02:02:00576 std::string manifest_key,
robf19335612015-01-07 00:38:35577 int options,
578 std::vector<InstallWarning>* warnings) {
Karan Bhatiad9937722018-10-24 02:40:15579 CSPParser csp_parser(policy);
abarth@chromium.orgee0be772011-12-02 08:02:10580
Antonio Sartori6eff34f62021-02-11 10:24:22581 bool allow_insecure_object_src = options & OPTIONS_ALLOW_INSECURE_OBJECT_SRC;
Karan Bhatiaf34832e82019-01-18 02:02:00582 ExtensionCSPEnforcer csp_enforcer(std::move(manifest_key),
583 allow_insecure_object_src, options);
Karan Bhatiad9937722018-10-24 02:40:15584 return csp_enforcer.Enforce(csp_parser.directives(), warnings);
lazyboyc0304f62017-01-03 21:17:30585}
abarth@chromium.orgee0be772011-12-02 08:02:10586
lazyboyc0304f62017-01-03 21:17:30587std::string GetEffectiveSandoxedPageCSP(const std::string& policy,
Karan Bhatiaf34832e82019-01-18 02:02:00588 std::string manifest_key,
lazyboyc0304f62017-01-03 21:17:30589 std::vector<InstallWarning>* warnings) {
Karan Bhatiad9937722018-10-24 02:40:15590 CSPParser csp_parser(policy);
Karan Bhatiaf34832e82019-01-18 02:02:00591 AppSandboxPageCSPEnforcer csp_enforcer(std::move(manifest_key));
Karan Bhatiad9937722018-10-24 02:40:15592 return csp_enforcer.Enforce(csp_parser.directives(), warnings);
abarth@chromium.orgee0be772011-12-02 08:02:10593}
594
mihaip@chromium.orgdbb24162012-06-06 01:41:22595bool ContentSecurityPolicyIsSandboxed(
yoz@chromium.org1d5e58b2013-01-31 08:41:40596 const std::string& policy, Manifest::Type type) {
mihaip@chromium.orgdbb24162012-06-06 01:41:22597 bool seen_sandbox = false;
Karan Bhatiad9937722018-10-24 02:40:15598 CSPParser parser(policy);
599 for (const auto& directive : parser.directives()) {
600 if (directive.directive_name != kSandboxDirectiveName)
mihaip@chromium.orgdbb24162012-06-06 01:41:22601 continue;
602
603 seen_sandbox = true;
604
Karan Bhatiad9937722018-10-24 02:40:15605 for (base::StringPiece token : directive.directive_values) {
606 std::string token_lower_case = base::ToLowerASCII(token);
mihaip@chromium.orgdbb24162012-06-06 01:41:22607
608 // The same origin token negates the sandboxing.
Karan Bhatiad9937722018-10-24 02:40:15609 if (token_lower_case == kAllowSameOriginToken)
mihaip@chromium.orgdbb24162012-06-06 01:41:22610 return false;
611
mek@chromium.orgf32e16f2012-09-27 20:41:17612 // Platform apps don't allow navigation.
Karan Bhatiad9937722018-10-24 02:40:15613 if (type == Manifest::TYPE_PLATFORM_APP &&
614 token_lower_case == kAllowTopNavigation) {
615 return false;
mihaip@chromium.orgdbb24162012-06-06 01:41:22616 }
617 }
618 }
619
620 return seen_sandbox;
621}
622
Karan Bhatia9f8b4ed2019-08-19 20:22:21623bool DoesCSPDisallowRemoteCode(const std::string& content_security_policy,
624 base::StringPiece manifest_key,
Jan Wilken Dörrie85285b02021-03-11 23:38:47625 std::u16string* error) {
Karan Bhatia11c67e592019-01-24 04:28:00626 DCHECK(error);
627
628 struct DirectiveMapping {
629 DirectiveMapping(DirectiveStatus status) : status(std::move(status)) {}
630
631 DirectiveStatus status;
632 const CSPParser::Directive* directive = nullptr;
633 };
634
635 DirectiveMapping script_src_mapping({DirectiveStatus({kScriptSrc})});
636 DirectiveMapping object_src_mapping({DirectiveStatus({kObjectSrc})});
637 DirectiveMapping worker_src_mapping({DirectiveStatus({kWorkerSrc})});
638 DirectiveMapping default_src_mapping({DirectiveStatus({kDefaultSrc})});
639
640 DirectiveMapping* directive_mappings[] = {
641 &script_src_mapping,
642 &object_src_mapping,
643 &worker_src_mapping,
644 &default_src_mapping,
645 };
646
647 // Populate |directive_mappings|.
Karan Bhatia9f8b4ed2019-08-19 20:22:21648 CSPParser csp_parser(content_security_policy);
Karan Bhatia11c67e592019-01-24 04:28:00649 for (DirectiveMapping* mapping : directive_mappings) {
650 // Find the first matching directive. As per
651 // http://www.w3.org/TR/CSP/#parse-a-csp-policy, duplicate directive names
652 // are ignored.
653 auto it = std::find_if(
654 csp_parser.directives().begin(), csp_parser.directives().end(),
655 [mapping](const CSPParser::Directive& directive) {
656 return mapping->status.Matches(directive.directive_name);
657 });
658
659 if (it != csp_parser.directives().end())
660 mapping->directive = &(*it);
661 }
662
663 auto fallback_if_necessary = [](DirectiveMapping* from,
664 const DirectiveMapping& to) {
665 DCHECK(from);
666
667 // No fallback necessary.
668 if (from->directive)
669 return;
670
671 from->directive = to.directive;
672 };
673
674 // "script-src" fallbacks to "default-src".
675 fallback_if_necessary(&script_src_mapping, default_src_mapping);
676
677 // "object-src" fallbacks to "default-src".
678 fallback_if_necessary(&object_src_mapping, default_src_mapping);
679
680 // "worker-src" fallbacks to "script-src", which might itself fallback to
681 // "default-src".
682 fallback_if_necessary(&worker_src_mapping, script_src_mapping);
683
Karan Bhatia9f8b4ed2019-08-19 20:22:21684 auto is_secure_directive = [manifest_key](const DirectiveMapping& mapping,
Jan Wilken Dörrie85285b02021-03-11 23:38:47685 std::u16string* error) {
Karan Bhatia11c67e592019-01-24 04:28:00686 if (!mapping.directive) {
687 *error = ErrorUtils::FormatErrorMessageUTF16(
Karan Bhatia9f8b4ed2019-08-19 20:22:21688 manifest_errors::kInvalidCSPMissingSecureSrc, manifest_key,
Karan Bhatia11c67e592019-01-24 04:28:00689 mapping.status.name());
690 return false;
691 }
692
693 auto directive_values = mapping.directive->directive_values;
694 auto it = std::find_if_not(
695 directive_values.begin(), directive_values.end(),
696 [](base::StringPiece source) {
697 std::string source_lower = base::ToLowerASCII(source);
698 return source_lower == kSelfSource || source_lower == kNoneSource ||
699 IsLocalHostSource(source_lower);
700 });
701
702 if (it == directive_values.end())
703 return true;
704
705 *error = ErrorUtils::FormatErrorMessageUTF16(
Karan Bhatia9f8b4ed2019-08-19 20:22:21706 manifest_errors::kInvalidCSPInsecureValueError, manifest_key, *it,
Karan Bhatia11c67e592019-01-24 04:28:00707 mapping.status.name());
708 return false;
709 };
710
711 std::set<const CSPParser::Directive*> secure_directives;
712 for (const DirectiveMapping* mapping : directive_mappings) {
713 // We don't need "default-src" to be a secure directive. Ignore it.
714 if (mapping == &default_src_mapping)
715 continue;
716
717 if (mapping->directive && secure_directives.count(mapping->directive)) {
718 // We already checked this directive and know it's secure. Skip it.
719 continue;
720 }
721
722 if (!is_secure_directive(*mapping, error))
723 return false;
724
725 DCHECK(mapping->directive);
726 secure_directives.insert(mapping->directive);
727 }
728
729 return true;
730}
731
limasdf@gmail.com244be132014-04-08 08:51:51732} // namespace csp_validator
abarth@chromium.orgee0be772011-12-02 08:02:10733
limasdf@gmail.com244be132014-04-08 08:51:51734} // namespace extensions