blob: b5bacd7c1853ed855cb3e3e111d12c73db70f827 [file] [log] [blame]
/*
* Copyright (C) 2011 Google, Inc. All rights reserved.
* Copyright (C) 2016 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY GOOGLE INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "ContentSecurityPolicySourceList.h"
#include "ContentSecurityPolicy.h"
#include "ContentSecurityPolicyDirectiveNames.h"
#include "ParsingUtilities.h"
#include "PublicSuffix.h"
#include <pal/text/TextEncoding.h>
#include <wtf/ASCIICType.h>
#include <wtf/NeverDestroyed.h>
#include <wtf/URL.h>
#include <wtf/text/Base64.h>
#include <wtf/text/StringParsingBuffer.h>
#include <wtf/text/StringToIntegerConversion.h>
namespace WebCore {
static bool isCSPDirectiveName(StringView name)
{
return equalIgnoringASCIICase(name, ContentSecurityPolicyDirectiveNames::baseURI)
|| equalIgnoringASCIICase(name, ContentSecurityPolicyDirectiveNames::connectSrc)
|| equalIgnoringASCIICase(name, ContentSecurityPolicyDirectiveNames::defaultSrc)
|| equalIgnoringASCIICase(name, ContentSecurityPolicyDirectiveNames::fontSrc)
|| equalIgnoringASCIICase(name, ContentSecurityPolicyDirectiveNames::formAction)
|| equalIgnoringASCIICase(name, ContentSecurityPolicyDirectiveNames::frameSrc)
|| equalIgnoringASCIICase(name, ContentSecurityPolicyDirectiveNames::imgSrc)
|| equalIgnoringASCIICase(name, ContentSecurityPolicyDirectiveNames::mediaSrc)
|| equalIgnoringASCIICase(name, ContentSecurityPolicyDirectiveNames::objectSrc)
|| equalIgnoringASCIICase(name, ContentSecurityPolicyDirectiveNames::pluginTypes)
|| equalIgnoringASCIICase(name, ContentSecurityPolicyDirectiveNames::reportURI)
|| equalIgnoringASCIICase(name, ContentSecurityPolicyDirectiveNames::sandbox)
|| equalIgnoringASCIICase(name, ContentSecurityPolicyDirectiveNames::scriptSrc)
|| equalIgnoringASCIICase(name, ContentSecurityPolicyDirectiveNames::styleSrc);
}
template<typename CharacterType> static bool isSourceCharacter(CharacterType c)
{
return !isASCIISpace(c);
}
template<typename CharacterType> static bool isHostCharacter(CharacterType c)
{
return isASCIIAlphanumeric(c) || c == '-';
}
template<typename CharacterType> static bool isPathComponentCharacter(CharacterType c)
{
return c != '?' && c != '#';
}
template<typename CharacterType> static bool isSchemeContinuationCharacter(CharacterType c)
{
return isASCIIAlphanumeric(c) || c == '+' || c == '-' || c == '.';
}
template<typename CharacterType> static bool isNotColonOrSlash(CharacterType c)
{
return c != ':' && c != '/';
}
template<typename CharacterType> static bool isSourceListNone(StringParsingBuffer<CharacterType> buffer)
{
skipWhile<isASCIISpace>(buffer);
if (!skipExactlyIgnoringASCIICase(buffer, "'none'"_s))
return false;
skipWhile<isASCIISpace>(buffer);
return buffer.atEnd();
}
ContentSecurityPolicySourceList::ContentSecurityPolicySourceList(const ContentSecurityPolicy& policy, const String& directiveName)
: m_policy(policy)
, m_directiveName(directiveName)
, m_contentSecurityPolicyModeForExtension(m_policy.contentSecurityPolicyModeForExtension())
{
}
void ContentSecurityPolicySourceList::parse(const String& value)
{
readCharactersForParsing(value, [&](auto buffer) {
if (isSourceListNone(buffer)) {
m_isNone = true;
return;
}
parse(buffer);
});
}
bool ContentSecurityPolicySourceList::isProtocolAllowedByStar(const URL& url) const
{
if (m_policy.allowContentSecurityPolicySourceStarToMatchAnyProtocol())
return true;
// This is counter to the CSP3 spec which only allows HTTPS but Chromium also allows it.
bool isAllowed = url.protocolIsInHTTPFamily() || url.protocolIs("ws"_s) || url.protocolIs("wss"_s) || url.protocolIs(m_policy.selfProtocol());
// Also not allowed by the Content Security Policy Level 3 spec., we allow a data URL to match
// "img-src *" and either a data URL or blob URL to match "media-src *" for web compatibility.
if (equalIgnoringASCIICase(m_directiveName, ContentSecurityPolicyDirectiveNames::imgSrc))
isAllowed |= url.protocolIsData();
else if (equalIgnoringASCIICase(m_directiveName, ContentSecurityPolicyDirectiveNames::mediaSrc))
isAllowed |= url.protocolIsData() || url.protocolIsBlob();
return isAllowed;
}
bool ContentSecurityPolicySourceList::matches(const URL& url, bool didReceiveRedirectResponse) const
{
if (m_allowStar && isProtocolAllowedByStar(url))
return true;
if (m_allowSelf && m_policy.urlMatchesSelf(url, equalIgnoringASCIICase(m_directiveName, ContentSecurityPolicyDirectiveNames::frameSrc)
))
return true;
for (auto& entry : m_list) {
if (entry.matches(url, didReceiveRedirectResponse))
return true;
}
return false;
}
bool ContentSecurityPolicySourceList::matches(const Vector<ContentSecurityPolicyHash>& hashes) const
{
for (auto& hash : hashes) {
if (m_hashes.contains(hash))
return true;
}
return false;
}
bool ContentSecurityPolicySourceList::matchesAll(const Vector<ContentSecurityPolicyHash>& hashes) const
{
if (hashes.isEmpty())
return false;
for (auto& hash : hashes) {
if (!m_hashes.contains(hash))
return false;
}
return true;
}
bool ContentSecurityPolicySourceList::matches(const String& nonce) const
{
if (nonce.isEmpty())
return false;
return m_nonces.contains(nonce);
}
static bool schemeIsInHttpFamily(StringView scheme)
{
return equalLettersIgnoringASCIICase(scheme, "https"_s) || equalLettersIgnoringASCIICase(scheme, "http"_s);
}
static bool isRestrictedDirectiveForMode(const String& directive, ContentSecurityPolicyModeForExtension mode)
{
switch (mode) {
case ContentSecurityPolicyModeForExtension::None:
return false;
// FIXME: If the script-src directive is strict enough, we should allow default-src to have more values.
case ContentSecurityPolicyModeForExtension::ManifestV2:
return directive == ContentSecurityPolicyDirectiveNames::scriptSrc
|| directive == ContentSecurityPolicyDirectiveNames::defaultSrc;
case ContentSecurityPolicyModeForExtension::ManifestV3:
return directive == ContentSecurityPolicyDirectiveNames::scriptSrc
|| directive == ContentSecurityPolicyDirectiveNames::objectSrc
|| directive == ContentSecurityPolicyDirectiveNames::workerSrc
|| directive == ContentSecurityPolicyDirectiveNames::defaultSrc;
}
return false;
}
bool ContentSecurityPolicySourceList::isValidSourceForExtensionMode(const ContentSecurityPolicySourceList::Source& parsedSource)
{
bool hostIsPublicSuffix = false;
#if ENABLE(PUBLIC_SUFFIX_LIST)
hostIsPublicSuffix = isPublicSuffix(parsedSource.host.value);
#endif
switch (m_contentSecurityPolicyModeForExtension) {
case ContentSecurityPolicyModeForExtension::None:
return true;
case ContentSecurityPolicyModeForExtension::ManifestV2:
if (!isRestrictedDirectiveForMode(m_directiveName, ContentSecurityPolicyModeForExtension::ManifestV2))
return true;
if (parsedSource.host.hasWildcard && hostIsPublicSuffix)
return false;
if (equalLettersIgnoringASCIICase(parsedSource.scheme, "blob"_s))
return true;
if (!equalLettersIgnoringASCIICase(parsedSource.scheme, "https"_s) || parsedSource.host.value.isEmpty())
return false;
break;
case ContentSecurityPolicyModeForExtension::ManifestV3:
if (!isRestrictedDirectiveForMode(m_directiveName, ContentSecurityPolicyModeForExtension::ManifestV3))
return true;
if (!schemeIsInHttpFamily(parsedSource.scheme) || !SecurityOrigin::isLocalHostOrLoopbackIPAddress(parsedSource.host.value))
return false;
}
return true;
}
static bool extensionModeAllowsKeywordsForDirective(ContentSecurityPolicyModeForExtension mode, const String& directiveName)
{
return mode != ContentSecurityPolicyModeForExtension::ManifestV3 || !isRestrictedDirectiveForMode(directiveName, mode);
}
// source-list = *WSP [ source *( 1*WSP source ) *WSP ]
// / *WSP "'none'" *WSP
//
template<typename CharacterType> void ContentSecurityPolicySourceList::parse(StringParsingBuffer<CharacterType> buffer)
{
while (buffer.hasCharactersRemaining()) {
skipWhile<isASCIISpace>(buffer);
if (buffer.atEnd())
return;
auto beginSource = buffer.position();
skipWhile<isSourceCharacter>(buffer);
auto sourceBuffer = StringParsingBuffer { beginSource, buffer.position() };
if (parseNonceSource(sourceBuffer))
continue;
if (parseHashSource(sourceBuffer))
continue;
if (auto source = parseSource(sourceBuffer)) {
// Wildcard hosts and keyword sources ('self', 'unsafe-inline',
// etc.) aren't stored in m_list, but as attributes on the source
// list itself.
if (source->scheme.isEmpty() && source->host.value.isEmpty())
continue;
if (isCSPDirectiveName(source->host.value))
m_policy.reportDirectiveAsSourceExpression(m_directiveName, source->host.value);
if (isValidSourceForExtensionMode(source.value()))
m_list.append(ContentSecurityPolicySource(m_policy, source->scheme.convertToASCIILowercase(), source->host.value.toString(), source->port.value, source->path, source->host.hasWildcard, source->port.hasWildcard, IsSelfSource::No));
} else
m_policy.reportInvalidSourceExpression(m_directiveName, String(beginSource, buffer.position() - beginSource));
ASSERT(buffer.atEnd() || isASCIISpace(*buffer));
}
m_list.shrinkToFit();
}
// source = scheme ":"
// / ( [ scheme "://" ] host [ port ] [ path ] )
// / "'self'"
//
template<typename CharacterType> std::optional<ContentSecurityPolicySourceList::Source> ContentSecurityPolicySourceList::parseSource(StringParsingBuffer<CharacterType> buffer)
{
if (buffer.atEnd())
return std::nullopt;
if (skipExactlyIgnoringASCIICase(buffer, "'none'"_s))
return std::nullopt;
Source source;
if (buffer.lengthRemaining() == 1 && *buffer == '*' && !isRestrictedDirectiveForMode(m_directiveName, m_contentSecurityPolicyModeForExtension)) {
m_allowStar = true;
return source;
}
if (skipExactlyIgnoringASCIICase(buffer, "'strict-dynamic'"_s)
&& extensionModeAllowsKeywordsForDirective(m_contentSecurityPolicyModeForExtension, m_directiveName)
&& (m_directiveName == ContentSecurityPolicyDirectiveNames::scriptSrc
|| m_directiveName == ContentSecurityPolicyDirectiveNames::scriptSrcElem)) {
m_allowNonParserInsertedScripts = true;
m_allowSelf = false;
m_allowInline = false;
return source;
}
if (skipExactlyIgnoringASCIICase(buffer, "'self'"_s)) {
m_allowSelf = !m_allowNonParserInsertedScripts;
return source;
}
if (skipExactlyIgnoringASCIICase(buffer, "'unsafe-inline'"_s) && !isRestrictedDirectiveForMode(m_directiveName, m_contentSecurityPolicyModeForExtension)) {
m_allowInline = !m_allowNonParserInsertedScripts;
return source;
}
if (skipExactlyIgnoringASCIICase(buffer, "'unsafe-eval'"_s) && extensionModeAllowsKeywordsForDirective(m_contentSecurityPolicyModeForExtension, m_directiveName)) {
m_allowEval = true;
m_allowWasmEval = true;
return source;
}
if (skipExactlyIgnoringASCIICase(buffer, "'wasm-unsafe-eval'"_s) && extensionModeAllowsKeywordsForDirective(m_contentSecurityPolicyModeForExtension, m_directiveName)) {
m_allowWasmEval = true;
return source;
}
if (skipExactlyIgnoringASCIICase(buffer, "'unsafe-hashes'"_s) && extensionModeAllowsKeywordsForDirective(m_contentSecurityPolicyModeForExtension, m_directiveName)) {
m_allowUnsafeHashes = true;
return source;
}
if (skipExactlyIgnoringASCIICase(buffer, "'report-sample'"_s) && extensionModeAllowsKeywordsForDirective(m_contentSecurityPolicyModeForExtension, m_directiveName)) {
m_reportSample = true;
return source;
}
if (m_allowNonParserInsertedScripts)
return source;
auto begin = buffer.position();
auto beginHost = begin;
auto beginPath = buffer.end();
const CharacterType* beginPort = nullptr;
skipWhile<isNotColonOrSlash>(buffer);
if (buffer.atEnd()) {
// host
// ^
auto host = parseHost(StringParsingBuffer { beginHost, buffer.position() });
if (!host)
return std::nullopt;
source.host = WTFMove(*host);
return source;
}
if (buffer.hasCharactersRemaining() && *buffer == '/') {
// host/path || host/ || /
// ^ ^ ^
auto host = parseHost(StringParsingBuffer { beginHost, buffer.position() });
if (!host)
return std::nullopt;
auto path = parsePath(buffer);
if (!path)
return std::nullopt;
source.host = WTFMove(*host);
source.path = WTFMove(path);
return source;
}
if (buffer.hasCharactersRemaining() && *buffer == ':') {
if (buffer.lengthRemaining() == 1) {
// scheme:
// ^
auto scheme = parseScheme(StringParsingBuffer { begin, buffer.position() });
if (!scheme)
return std::nullopt;
source.scheme = WTFMove(scheme);
return source;
}
if (buffer[1] == '/') {
// scheme://host || scheme://
// ^ ^
auto scheme = parseScheme(StringParsingBuffer { begin, buffer.position() });
if (!scheme
|| !skipExactly(buffer, ':')
|| !skipExactly(buffer, '/')
|| !skipExactly(buffer, '/'))
return std::nullopt;
if (buffer.atEnd())
return std::nullopt;
source.scheme = WTFMove(scheme);
beginHost = buffer.position();
skipWhile<isNotColonOrSlash>(buffer);
}
if (buffer.hasCharactersRemaining() && *buffer == ':') {
// host:port || scheme://host:port
// ^ ^
beginPort = buffer.position();
skipUntil(buffer, '/');
}
}
if (buffer.hasCharactersRemaining() && *buffer == '/') {
// scheme://host/path || scheme://host:port/path
// ^ ^
if (buffer.position() == beginHost)
return std::nullopt;
beginPath = buffer.position();
}
auto host = parseHost(StringParsingBuffer { beginHost, beginPort ? beginPort : beginPath });
if (!host)
return std::nullopt;
if (beginPort) {
auto port = parsePort(StringParsingBuffer { beginPort, beginPath });
if (!port)
return std::nullopt;
source.port = WTFMove(*port);
}
if (beginPath != buffer.end()) {
auto path = parsePath(StringParsingBuffer { beginPath, buffer.end() });
if (!path)
return std::nullopt;
source.path = WTFMove(path);
}
source.host = WTFMove(*host);
return source;
}
// ; <scheme> production from RFC 3986
// scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
//
template<typename CharacterType> StringView ContentSecurityPolicySourceList::parseScheme(StringParsingBuffer<CharacterType> buffer)
{
ASSERT(buffer.position() <= buffer.end());
if (buffer.atEnd())
return { };
auto begin = buffer.position();
if (!skipExactly<isASCIIAlpha>(buffer))
return { };
skipWhile<isSchemeContinuationCharacter>(buffer);
if (!buffer.atEnd())
return { };
return StringView(begin, buffer.position() - begin);
}
// host = [ "*." ] 1*host-char *( "." 1*host-char )
// / "*"
// host-char = ALPHA / DIGIT / "-"
//
template<typename CharacterType> std::optional<ContentSecurityPolicySourceList::Host> ContentSecurityPolicySourceList::parseHost(StringParsingBuffer<CharacterType> buffer)
{
ASSERT(buffer.position() <= buffer.end());
if (buffer.atEnd())
return std::nullopt;
Host host;
if (skipExactly(buffer, '*')) {
host.hasWildcard = true;
if (buffer.atEnd())
return host;
if (!skipExactly(buffer, '.'))
return std::nullopt;
}
auto hostBegin = buffer.position();
while (buffer.hasCharactersRemaining()) {
if (!skipExactly<isHostCharacter>(buffer))
return std::nullopt;
skipWhile<isHostCharacter>(buffer);
if (buffer.hasCharactersRemaining() && !skipExactly(buffer, '.'))
return std::nullopt;
}
ASSERT(buffer.atEnd());
host.value = StringView(hostBegin, buffer.position() - hostBegin);
return host;
}
template<typename CharacterType> String ContentSecurityPolicySourceList::parsePath(StringParsingBuffer<CharacterType> buffer)
{
ASSERT(buffer.position() <= buffer.end());
auto begin = buffer.position();
skipWhile<isPathComponentCharacter>(buffer);
// path/to/file.js?query=string || path/to/file.js#anchor
// ^ ^
if (buffer.hasCharactersRemaining())
m_policy.reportInvalidPathCharacter(m_directiveName, String(begin, buffer.end() - begin), *buffer);
ASSERT(buffer.position() <= buffer.end());
ASSERT(buffer.atEnd() || (*buffer == '#' || *buffer == '?'));
return PAL::decodeURLEscapeSequences(StringView(begin, buffer.position() - begin));
}
// port = ":" ( 1*DIGIT / "*" )
//
template<typename CharacterType> std::optional<ContentSecurityPolicySourceList::Port> ContentSecurityPolicySourceList::parsePort(StringParsingBuffer<CharacterType> buffer)
{
ASSERT(buffer.position() <= buffer.end());
if (!skipExactly(buffer, ':'))
ASSERT_NOT_REACHED();
if (buffer.atEnd())
return std::nullopt;
if (buffer.lengthRemaining() == 1 && *buffer == '*') {
Port port;
port.hasWildcard = true;
return port;
}
auto begin = buffer.position();
skipWhile<isASCIIDigit>(buffer);
if (!buffer.atEnd())
return std::nullopt;
unsigned length = buffer.position() - begin;
auto portInteger = parseInteger<uint16_t>({ begin, length }).value_or(0);
if (!portInteger)
return std::nullopt;
Port port;
port.value = portInteger;
return port;
}
// Match Blink's behavior of allowing an equal sign to appear anywhere in the value of the nonce
// even though this does not match the behavior of Content Security Policy Level 3 spec.,
// <https://w3c.github.io/webappsec-csp/> (29 February 2016).
template<typename CharacterType> static bool isNonceCharacter(CharacterType c)
{
return isBase64OrBase64URLCharacter(c) || c == '=';
}
// nonce-source = "'nonce-" nonce-value "'"
// nonce-value = base64-value
template<typename CharacterType> bool ContentSecurityPolicySourceList::parseNonceSource(StringParsingBuffer<CharacterType> buffer)
{
if (!skipExactlyIgnoringASCIICase(buffer, "'nonce-"_s))
return false;
auto beginNonceValue = buffer.position();
skipWhile<isNonceCharacter>(buffer);
if (buffer.atEnd() || buffer.position() == beginNonceValue || *buffer != '\'')
return false;
if (extensionModeAllowsKeywordsForDirective(m_contentSecurityPolicyModeForExtension, m_directiveName))
m_nonces.add(String(beginNonceValue, buffer.position() - beginNonceValue));
return true;
}
// hash-source = "'" hash-algorithm "-" base64-value "'"
// hash-algorithm = "sha256" / "sha384" / "sha512"
// base64-value = 1*( ALPHA / DIGIT / "+" / "/" / "-" / "_" )*2( "=" )
template<typename CharacterType> bool ContentSecurityPolicySourceList::parseHashSource(StringParsingBuffer<CharacterType> buffer)
{
if (buffer.atEnd())
return false;
if (!skipExactly(buffer, '\''))
return false;
auto digest = parseCryptographicDigest(buffer);
if (!digest)
return false;
if (buffer.atEnd() || *buffer != '\'')
return false;
if (digest->value.size() > ContentSecurityPolicyHash::maximumDigestLength)
return false;
if (extensionModeAllowsKeywordsForDirective(m_contentSecurityPolicyModeForExtension, m_directiveName)) {
m_hashAlgorithmsUsed.add(digest->algorithm);
m_hashes.add(WTFMove(*digest));
}
return true;
}
} // namespace WebCore