blob: 17ae1426ef9662f6f640c36d8ae8bca8e443a15a [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/core/frame/csp/source_list_directive.h"
#include "third_party/blink/renderer/core/frame/csp/content_security_policy.h"
#include "third_party/blink/renderer/core/frame/csp/csp_source.h"
#include "third_party/blink/renderer/platform/network/content_security_policy_parsers.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/weborigin/kurl.h"
#include "third_party/blink/renderer/platform/weborigin/security_origin.h"
#include "third_party/blink/renderer/platform/wtf/text/base64.h"
#include "third_party/blink/renderer/platform/wtf/text/parsing_utilities.h"
#include "third_party/blink/renderer/platform/wtf/text/string_to_number.h"
#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
namespace {
struct SupportedPrefixesStruct {
const char* prefix;
network::mojom::blink::CSPHashAlgorithm type;
};
} // namespace
namespace blink {
namespace {
bool IsSourceListNone(const UChar* begin, const UChar* end) {
SkipWhile<UChar, IsASCIISpace>(begin, end);
const UChar* position = begin;
SkipWhile<UChar, IsSourceCharacter>(position, end);
if (!EqualIgnoringASCIICase(
"'none'",
StringView(begin, static_cast<wtf_size_t>(position - begin))))
return false;
SkipWhile<UChar, IsASCIISpace>(position, end);
if (position != end)
return false;
return true;
}
// nonce-source = "'nonce-" nonce-value "'"
// nonce-value = 1*( ALPHA / DIGIT / "+" / "/" / "=" )
//
bool ParseNonce(const UChar* begin, const UChar* end, String* nonce) {
size_t nonce_length = end - begin;
StringView prefix("'nonce-");
// TODO(esprehn): Should be StringView(begin, nonceLength).startsWith(prefix).
if (nonce_length <= prefix.length() ||
!EqualIgnoringASCIICase(prefix, StringView(begin, prefix.length()))) {
return true;
}
const UChar* position = begin + prefix.length();
const UChar* nonce_begin = position;
DCHECK(position < end);
SkipWhile<UChar, IsNonceCharacter>(position, end);
DCHECK(nonce_begin <= position);
if (position + 1 != end || *position != '\'' || position == nonce_begin)
return false;
*nonce = String(nonce_begin, static_cast<wtf_size_t>(position - nonce_begin));
return true;
}
// hash-source = "'" hash-algorithm "-" hash-value "'"
// hash-algorithm = "sha1" / "sha256" / "sha384" / "sha512"
// hash-value = 1*( ALPHA / DIGIT / "+" / "/" / "=" )
//
bool ParseHash(const UChar* begin,
const UChar* end,
Vector<uint8_t>& hash,
network::mojom::blink::CSPHashAlgorithm* hash_algorithm) {
// Any additions or subtractions from this struct should also modify the
// respective entries in the kAlgorithmMap array in
// ContentSecurityPolicy::FillInCSPHashValues().
constexpr SupportedPrefixesStruct kSupportedPrefixes[] = {
{"'sha256-", network::mojom::blink::CSPHashAlgorithm::SHA256},
{"'sha384-", network::mojom::blink::CSPHashAlgorithm::SHA384},
{"'sha512-", network::mojom::blink::CSPHashAlgorithm::SHA512},
{"'sha-256-", network::mojom::blink::CSPHashAlgorithm::SHA256},
{"'sha-384-", network::mojom::blink::CSPHashAlgorithm::SHA384},
{"'sha-512-", network::mojom::blink::CSPHashAlgorithm::SHA512}};
StringView prefix;
size_t hash_length = end - begin;
DCHECK_EQ(*hash_algorithm, network::mojom::blink::CSPHashAlgorithm::None);
for (auto supported_prefix : kSupportedPrefixes) {
prefix = supported_prefix.prefix;
// TODO(esprehn): Should be StringView(begin, end -
// begin).startsWith(prefix).
if (hash_length > prefix.length() &&
EqualIgnoringASCIICase(prefix, StringView(begin, prefix.length()))) {
*hash_algorithm = supported_prefix.type;
break;
}
}
if (*hash_algorithm == network::mojom::blink::CSPHashAlgorithm::None)
return true;
const UChar* position = begin + prefix.length();
const UChar* hash_begin = position;
DCHECK(position < end);
SkipWhile<UChar, IsBase64EncodedCharacter>(position, end);
DCHECK(hash_begin <= position);
// Base64 encodings may end with exactly one or two '=' characters
if (position < end)
SkipExactly<UChar>(position, position + 1, '=');
if (position < end)
SkipExactly<UChar>(position, position + 1, '=');
if (position + 1 != end || *position != '\'' || position == hash_begin)
return false;
// We accept base64url-encoded data here by normalizing it to base64.
Vector<char> out;
Base64Decode(NormalizeToBase64(String(
hash_begin, static_cast<wtf_size_t>(position - hash_begin))),
out);
if (out.size() > kMaxDigestSize)
return false;
DCHECK(hash.IsEmpty());
for (char el : out)
hash.push_back(el);
return true;
}
// ; <scheme> production from RFC 3986
// scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
//
bool ParseScheme(const UChar* begin, const UChar* end, String* scheme) {
DCHECK(begin <= end);
DCHECK(scheme->IsEmpty());
if (begin == end)
return false;
const UChar* position = begin;
if (!SkipExactly<UChar, IsASCIIAlpha>(position, end))
return false;
SkipWhile<UChar, IsSchemeContinuationCharacter>(position, end);
if (position != end)
return false;
*scheme = String(begin, static_cast<wtf_size_t>(end - begin));
return true;
}
// host = [ "*." ] 1*host-char *( "." 1*host-char )
// / "*"
// host-char = ALPHA / DIGIT / "-"
//
// static
bool ParseHost(const UChar* begin,
const UChar* end,
String* host,
bool* host_wildcard) {
DCHECK(begin <= end);
DCHECK(host->IsEmpty());
DCHECK(!*host_wildcard);
if (begin == end)
return false;
const UChar* position = begin;
// Parse "*" or [ "*." ].
if (SkipExactly<UChar>(position, end, '*')) {
*host_wildcard = true;
if (position == end) {
// "*"
return true;
}
if (!SkipExactly<UChar>(position, end, '.'))
return false;
}
const UChar* host_begin = position;
// Parse 1*host-hcar.
if (!SkipExactly<UChar, IsHostCharacter>(position, end))
return false;
SkipWhile<UChar, IsHostCharacter>(position, end);
// Parse *( "." 1*host-char ).
while (position < end) {
if (!SkipExactly<UChar>(position, end, '.'))
return false;
if (!SkipExactly<UChar, IsHostCharacter>(position, end))
return false;
SkipWhile<UChar, IsHostCharacter>(position, end);
}
*host = String(host_begin, static_cast<wtf_size_t>(end - host_begin));
return true;
}
bool ParsePath(const UChar* begin,
const UChar* end,
String* path,
ContentSecurityPolicy* policy,
const String& directive_name) {
DCHECK(begin <= end);
DCHECK(path->IsEmpty());
const UChar* position = begin;
SkipWhile<UChar, IsPathComponentCharacter>(position, end);
// path/to/file.js?query=string || path/to/file.js#anchor
// ^ ^
if (position < end) {
policy->ReportInvalidPathCharacter(
directive_name, String(begin, static_cast<wtf_size_t>(end - begin)),
*position);
}
*path = DecodeURLEscapeSequences(
String(begin, static_cast<wtf_size_t>(position - begin)),
DecodeURLMode::kUTF8OrIsomorphic);
DCHECK(position <= end);
DCHECK(position == end || (*position == '#' || *position == '?'));
return true;
}
// port = ":" ( 1*DIGIT / "*" )
//
bool ParsePort(const UChar* begin,
const UChar* end,
int* port,
bool* port_wildcard) {
DCHECK(begin <= end);
DCHECK_EQ(*port, url::PORT_UNSPECIFIED);
DCHECK(!*port_wildcard);
if (!SkipExactly<UChar>(begin, end, ':'))
NOTREACHED();
if (begin == end)
return false;
if (end - begin == 1 && *begin == '*') {
*port = url::PORT_UNSPECIFIED;
*port_wildcard = true;
return true;
}
const UChar* position = begin;
SkipWhile<UChar, IsASCIIDigit>(position, end);
if (position != end)
return false;
bool ok;
*port = CharactersToInt(begin, end - begin, WTF::NumberParsingOptions::kNone,
&ok);
return ok;
}
// source = scheme ":"
// / ( [ scheme "://" ] host [ port ] [ path ] )
// / "'self'"
bool ParseSourceExpression(const UChar* begin,
const UChar* end,
ContentSecurityPolicy* policy,
const String& directive_name,
network::mojom::blink::CSPSource& parsed_source) {
const UChar* position = begin;
const UChar* begin_host = begin;
const UChar* begin_path = end;
const UChar* begin_port = nullptr;
SkipWhile<UChar, IsNotColonOrSlash>(position, end);
if (position == end) {
// host
// ^
return ParseHost(begin_host, position, &parsed_source.host,
&parsed_source.is_host_wildcard);
}
if (position < end && *position == '/') {
// host/path || host/ || /
// ^ ^ ^
return ParseHost(begin_host, position, &parsed_source.host,
&parsed_source.is_host_wildcard) &&
ParsePath(position, end, &parsed_source.path, policy,
directive_name);
}
if (position < end && *position == ':') {
if (end - position == 1) {
// scheme:
// ^
return ParseScheme(begin, position, &parsed_source.scheme);
}
if (position[1] == '/') {
// scheme://host || scheme://
// ^ ^
if (!ParseScheme(begin, position, &parsed_source.scheme) ||
!SkipExactly<UChar>(position, end, ':') ||
!SkipExactly<UChar>(position, end, '/') ||
!SkipExactly<UChar>(position, end, '/'))
return false;
if (position == end)
return false;
begin_host = position;
SkipWhile<UChar, IsNotColonOrSlash>(position, end);
}
if (position < end && *position == ':') {
// host:port || scheme://host:port
// ^ ^
begin_port = position;
SkipUntil<UChar>(position, end, '/');
}
}
if (position < end && *position == '/') {
// scheme://host/path || scheme://host:port/path
// ^ ^
if (position == begin_host)
return false;
begin_path = position;
}
if (!ParseHost(begin_host, begin_port ? begin_port : begin_path,
&parsed_source.host, &parsed_source.is_host_wildcard))
return false;
if (begin_port) {
if (!ParsePort(begin_port, begin_path, &parsed_source.port,
&parsed_source.is_port_wildcard))
return false;
} else {
parsed_source.port = url::PORT_UNSPECIFIED;
}
if (begin_path != end) {
if (!ParsePath(begin_path, end, &parsed_source.path, policy,
directive_name))
return false;
}
return true;
}
bool ParseSource(const UChar* begin,
const UChar* end,
network::mojom::blink::CSPSourceList& source_list,
ContentSecurityPolicy* policy,
const String& directive_name) {
if (begin == end)
return false;
StringView token(begin, static_cast<wtf_size_t>(end - begin));
if (EqualIgnoringASCIICase("'none'", token))
return false;
if (end - begin == 1 && *begin == '*') {
source_list.allow_star = true;
return true;
}
if (EqualIgnoringASCIICase("'self'", token)) {
source_list.allow_self = true;
return true;
}
if (EqualIgnoringASCIICase("'unsafe-inline'", token)) {
source_list.allow_inline = true;
return true;
}
if (EqualIgnoringASCIICase("'unsafe-eval'", token)) {
source_list.allow_eval = true;
return true;
}
if (EqualIgnoringASCIICase("'unsafe-allow-redirects'", token)) {
source_list.allow_response_redirects = true;
return true;
}
if (policy->SupportsWasmEval() &&
EqualIgnoringASCIICase("'wasm-eval'", token)) {
source_list.allow_wasm_eval = true;
return true;
}
if (EqualIgnoringASCIICase("'strict-dynamic'", token)) {
source_list.allow_dynamic = true;
return true;
}
if (EqualIgnoringASCIICase("'unsafe-hashes'", token)) {
source_list.allow_unsafe_hashes = true;
return true;
}
if (EqualIgnoringASCIICase("'report-sample'", token)) {
source_list.report_sample = true;
return true;
}
String nonce;
if (!ParseNonce(begin, end, &nonce))
return false;
if (!nonce.IsNull()) {
source_list.nonces.push_back(nonce);
return true;
}
Vector<uint8_t> hash;
network::mojom::blink::CSPHashAlgorithm algorithm =
network::mojom::blink::CSPHashAlgorithm::None;
if (!ParseHash(begin, end, hash, &algorithm))
return false;
if (hash.size() > 0) {
source_list.hashes.push_back(
network::mojom::blink::CSPHashSource::New(algorithm, hash));
return true;
}
// We must initialize all fields of |source_expression| so that it is always
// valid for serialization by mojo, even if scheme, host or path are not
// provided.
auto source_expression =
network::mojom::blink::CSPSource::New("", "", -1, "", false, false);
if (ParseSourceExpression(begin, end, policy, directive_name,
*source_expression)) {
source_list.sources.push_back(std::move(source_expression));
return true;
}
return false;
}
// source-list = *WSP [ source *( 1*WSP source ) *WSP ]
// / *WSP "'none'" *WSP
//
network::mojom::blink::CSPSourceListPtr Parse(const UChar* begin,
const UChar* end,
ContentSecurityPolicy* policy,
const String& directive_name) {
network::mojom::blink::CSPSourceListPtr source_list =
network::mojom::blink::CSPSourceList::New();
if (IsSourceListNone(begin, end))
return source_list;
const UChar* position = begin;
while (position < end) {
SkipWhile<UChar, IsASCIISpace>(position, end);
if (position == end)
return source_list;
const UChar* begin_source = position;
SkipWhile<UChar, IsSourceCharacter>(position, end);
if (ParseSource(begin_source, position, *source_list, policy,
directive_name)) {
String token(begin_source,
static_cast<wtf_size_t>(position - begin_source));
if (ContentSecurityPolicy::GetDirectiveType(token) !=
CSPDirectiveName::Unknown) {
policy->ReportDirectiveAsSourceExpression(
directive_name,
source_list->sources[source_list->sources.size() - 1]->host);
}
} else {
policy->ReportInvalidSourceExpression(
directive_name, String(begin_source, static_cast<wtf_size_t>(
position - begin_source)));
}
DCHECK(position == end || IsASCIISpace(*position));
}
return source_list;
}
bool HasSourceMatchInList(
const Vector<network::mojom::blink::CSPSourcePtr>& list,
const String& self_protocol,
const KURL& url,
ResourceRequest::RedirectStatus redirect_status) {
for (const auto& source : list) {
if (CSPSourceMatches(*source, self_protocol, url, redirect_status)) {
return true;
}
}
return false;
}
} // namespace
network::mojom::blink::CSPSourceListPtr CSPSourceListParse(
const String& name,
const String& value,
ContentSecurityPolicy* policy) {
Vector<UChar> characters;
value.AppendTo(characters);
return Parse(characters.data(), characters.data() + characters.size(), policy,
name);
}
bool CSPSourceListAllows(
const network::mojom::blink::CSPSourceList& source_list,
const network::mojom::blink::CSPSource& self_source,
const KURL& url,
ResourceRequest::RedirectStatus redirect_status) {
// Wildcards match network schemes ('http', 'https', 'ftp', 'ws', 'wss'), and
// the scheme of the protected resource:
// https://w3c.github.io/webappsec-csp/#match-url-to-source-expression. Other
// schemes, including custom schemes, must be explicitly listed in a source
// list.
if (source_list.allow_star) {
if (url.ProtocolIsInHTTPFamily() || url.ProtocolIs("ftp") ||
url.ProtocolIs("ws") || url.ProtocolIs("wss") ||
(!url.Protocol().IsEmpty() &&
EqualIgnoringASCIICase(url.Protocol(), self_source.scheme)))
return true;
return HasSourceMatchInList(source_list.sources, self_source.scheme, url,
redirect_status);
}
if (source_list.allow_self && CSPSourceMatchesAsSelf(self_source, url)) {
return true;
}
return HasSourceMatchInList(source_list.sources, self_source.scheme, url,
redirect_status);
}
bool CSPSourceListAllowNonce(
const network::mojom::blink::CSPSourceList& source_list,
const String& nonce) {
String nonce_stripped = nonce.StripWhiteSpace();
return !nonce_stripped.IsNull() &&
source_list.nonces.Contains(nonce_stripped);
}
bool CSPSourceListAllowHash(
const network::mojom::blink::CSPSourceList& source_list,
const network::mojom::blink::CSPHashSource& hash_value) {
for (const network::mojom::blink::CSPHashSourcePtr& hash :
source_list.hashes) {
if (*hash == hash_value)
return true;
}
return false;
}
bool CSPSourceListIsNone(
const network::mojom::blink::CSPSourceList& source_list) {
return !source_list.sources.size() && !source_list.allow_self &&
!source_list.allow_star && !source_list.allow_inline &&
!source_list.allow_unsafe_hashes && !source_list.allow_eval &&
!source_list.allow_wasm_eval && !source_list.allow_dynamic &&
!source_list.nonces.size() && !source_list.hashes.size();
}
bool CSPSourceListIsSelf(
const network::mojom::blink::CSPSourceList& source_list) {
return source_list.allow_self && !source_list.sources.size() &&
!source_list.allow_star && !source_list.allow_inline &&
!source_list.allow_unsafe_hashes && !source_list.allow_eval &&
!source_list.allow_wasm_eval && !source_list.allow_dynamic &&
!source_list.nonces.size() && !source_list.hashes.size();
}
bool CSPSourceListIsHashOrNoncePresent(
const network::mojom::blink::CSPSourceList& source_list) {
return !source_list.nonces.IsEmpty() || !source_list.hashes.IsEmpty();
}
bool CSPSourceListAllowsURLBasedMatching(
const network::mojom::blink::CSPSourceList& source_list) {
return !source_list.allow_dynamic &&
(source_list.sources.size() || source_list.allow_star ||
source_list.allow_self);
}
bool CSPSourceListAllowAllInline(
CSPDirectiveName directive_type,
const network::mojom::blink::CSPSourceList& source_list) {
if (directive_type != CSPDirectiveName::DefaultSrc &&
!ContentSecurityPolicy::IsScriptDirective(directive_type) &&
!ContentSecurityPolicy::IsStyleDirective(directive_type)) {
return false;
}
return source_list.allow_inline &&
!CSPSourceListIsHashOrNoncePresent(source_list) &&
(!ContentSecurityPolicy::IsScriptDirective(directive_type) ||
!source_list.allow_dynamic);
}
} // namespace blink