| // 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 |