blob: b8af12ea2e9349ab11e08ff481ad7b6570312ff1 [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 "TextEncoding.h"
#include <wtf/ASCIICType.h>
#include <wtf/NeverDestroyed.h>
#include <wtf/URL.h>
#include <wtf/text/Base64.h>
namespace WebCore {
static bool isCSPDirectiveName(const String& 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);
}
static bool isSourceCharacter(UChar c)
{
return !isASCIISpace(c);
}
static bool isHostCharacter(UChar c)
{
return isASCIIAlphanumeric(c) || c == '-';
}
static bool isPathComponentCharacter(UChar c)
{
return c != '?' && c != '#';
}
static bool isSchemeContinuationCharacter(UChar c)
{
return isASCIIAlphanumeric(c) || c == '+' || c == '-' || c == '.';
}
static bool isNotColonOrSlash(UChar c)
{
return c != ':' && c != '/';
}
static bool isSourceListNone(const String& value)
{
auto characters = StringView(value).upconvertedCharacters();
const UChar* begin = characters;
const UChar* end = characters + value.length();
skipWhile<UChar, isASCIISpace>(begin, end);
const UChar* position = begin;
skipWhile<UChar, isSourceCharacter>(position, end);
if (!equalLettersIgnoringASCIICase(begin, position - begin, "'none'"))
return false;
skipWhile<UChar, isASCIISpace>(position, end);
if (position != end)
return false;
return true;
}
ContentSecurityPolicySourceList::ContentSecurityPolicySourceList(const ContentSecurityPolicy& policy, const String& directiveName)
: m_policy(policy)
, m_directiveName(directiveName)
{
}
void ContentSecurityPolicySourceList::parse(const String& value)
{
if (isSourceListNone(value)) {
m_isNone = true;
return;
}
auto characters = StringView(value).upconvertedCharacters();
parse(characters, characters + value.length());
}
bool ContentSecurityPolicySourceList::isProtocolAllowedByStar(const URL& url) const
{
if (m_policy.allowContentSecurityPolicySourceStarToMatchAnyProtocol())
return true;
// Although 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.
bool isAllowed = url.protocolIsInHTTPFamily() || url.protocolIs("ws") || url.protocolIs("wss") || m_policy.protocolMatchesSelf(url);
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))
return true;
for (auto& entry : m_list) {
if (entry.matches(url, didReceiveRedirectResponse))
return true;
}
return false;
}
bool ContentSecurityPolicySourceList::matches(const ContentSecurityPolicyHash& hash) const
{
return m_hashes.contains(hash);
}
bool ContentSecurityPolicySourceList::matches(const String& nonce) const
{
return m_nonces.contains(nonce);
}
// source-list = *WSP [ source *( 1*WSP source ) *WSP ]
// / *WSP "'none'" *WSP
//
void ContentSecurityPolicySourceList::parse(const UChar* begin, const UChar* end)
{
const UChar* position = begin;
while (position < end) {
skipWhile<UChar, isASCIISpace>(position, end);
if (position == end)
return;
const UChar* beginSource = position;
skipWhile<UChar, isSourceCharacter>(position, end);
String scheme, host, path;
Optional<uint16_t> port;
bool hostHasWildcard = false;
bool portHasWildcard = false;
if (parseNonceSource(beginSource, position))
continue;
if (parseHashSource(beginSource, position))
continue;
if (parseSource(beginSource, position, scheme, host, port, path, hostHasWildcard, portHasWildcard)) {
// Wildcard hosts and keyword sources ('self', 'unsafe-inline',
// etc.) aren't stored in m_list, but as attributes on the source
// list itself.
if (scheme.isEmpty() && host.isEmpty())
continue;
if (isCSPDirectiveName(host))
m_policy.reportDirectiveAsSourceExpression(m_directiveName, host);
m_list.append(ContentSecurityPolicySource(m_policy, scheme, host, port, path, hostHasWildcard, portHasWildcard));
} else
m_policy.reportInvalidSourceExpression(m_directiveName, String(beginSource, position - beginSource));
ASSERT(position == end || isASCIISpace(*position));
}
m_list.shrinkToFit();
}
// source = scheme ":"
// / ( [ scheme "://" ] host [ port ] [ path ] )
// / "'self'"
//
bool ContentSecurityPolicySourceList::parseSource(const UChar* begin, const UChar* end, String& scheme, String& host, Optional<uint16_t>& port, String& path, bool& hostHasWildcard, bool& portHasWildcard)
{
if (begin == end)
return false;
if (equalLettersIgnoringASCIICase(begin, end - begin, "'none'"))
return false;
if (end - begin == 1 && *begin == '*') {
m_allowStar = true;
return true;
}
if (equalLettersIgnoringASCIICase(begin, end - begin, "'self'")) {
m_allowSelf = true;
return true;
}
if (equalLettersIgnoringASCIICase(begin, end - begin, "'unsafe-inline'")) {
m_allowInline = true;
return true;
}
if (equalLettersIgnoringASCIICase(begin, end - begin, "'unsafe-eval'")) {
m_allowEval = true;
return true;
}
const UChar* position = begin;
const UChar* beginHost = begin;
const UChar* beginPath = end;
const UChar* beginPort = nullptr;
skipWhile<UChar, isNotColonOrSlash>(position, end);
if (position == end) {
// host
// ^
return parseHost(beginHost, position, host, hostHasWildcard);
}
if (position < end && *position == '/') {
// host/path || host/ || /
// ^ ^ ^
return parseHost(beginHost, position, host, hostHasWildcard) && parsePath(position, end, path);
}
if (position < end && *position == ':') {
if (end - position == 1) {
// scheme:
// ^
return parseScheme(begin, position, scheme);
}
if (position[1] == '/') {
// scheme://host || scheme://
// ^ ^
if (!parseScheme(begin, position, scheme)
|| !skipExactly<UChar>(position, end, ':')
|| !skipExactly<UChar>(position, end, '/')
|| !skipExactly<UChar>(position, end, '/'))
return false;
if (position == end)
return false;
beginHost = position;
skipWhile<UChar, isNotColonOrSlash>(position, end);
}
if (position < end && *position == ':') {
// host:port || scheme://host:port
// ^ ^
beginPort = position;
skipUntil<UChar>(position, end, '/');
}
}
if (position < end && *position == '/') {
// scheme://host/path || scheme://host:port/path
// ^ ^
if (position == beginHost)
return false;
beginPath = position;
}
if (!parseHost(beginHost, beginPort ? beginPort : beginPath, host, hostHasWildcard))
return false;
if (!beginPort)
port = WTF::nullopt;
else {
if (!parsePort(beginPort, beginPath, port, portHasWildcard))
return false;
}
if (beginPath != end) {
if (!parsePath(beginPath, end, path))
return false;
}
return true;
}
// ; <scheme> production from RFC 3986
// scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
//
bool ContentSecurityPolicySourceList::parseScheme(const UChar* begin, const UChar* end, String& scheme)
{
ASSERT(begin <= end);
ASSERT(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, end - begin);
return true;
}
// host = [ "*." ] 1*host-char *( "." 1*host-char )
// / "*"
// host-char = ALPHA / DIGIT / "-"
//
bool ContentSecurityPolicySourceList::parseHost(const UChar* begin, const UChar* end, String& host, bool& hostHasWildcard)
{
ASSERT(begin <= end);
ASSERT(host.isEmpty());
ASSERT(!hostHasWildcard);
if (begin == end)
return false;
const UChar* position = begin;
if (skipExactly<UChar>(position, end, '*')) {
hostHasWildcard = true;
if (position == end)
return true;
if (!skipExactly<UChar>(position, end, '.'))
return false;
}
const UChar* hostBegin = position;
while (position < end) {
if (!skipExactly<UChar, isHostCharacter>(position, end))
return false;
skipWhile<UChar, isHostCharacter>(position, end);
if (position < end && !skipExactly<UChar>(position, end, '.'))
return false;
}
ASSERT(position == end);
host = String(hostBegin, end - hostBegin);
return true;
}
bool ContentSecurityPolicySourceList::parsePath(const UChar* begin, const UChar* end, String& path)
{
ASSERT(begin <= end);
ASSERT(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)
m_policy.reportInvalidPathCharacter(m_directiveName, String(begin, end - begin), *position);
path = decodeURLEscapeSequences(String(begin, position - begin));
ASSERT(position <= end);
ASSERT(position == end || (*position == '#' || *position == '?'));
return true;
}
// port = ":" ( 1*DIGIT / "*" )
//
bool ContentSecurityPolicySourceList::parsePort(const UChar* begin, const UChar* end, Optional<uint16_t>& port, bool& portHasWildcard)
{
ASSERT(begin <= end);
ASSERT(!port);
ASSERT(!portHasWildcard);
if (!skipExactly<UChar>(begin, end, ':'))
ASSERT_NOT_REACHED();
if (begin == end)
return false;
if (end - begin == 1 && *begin == '*') {
port = WTF::nullopt;
portHasWildcard = true;
return true;
}
const UChar* position = begin;
skipWhile<UChar, isASCIIDigit>(position, end);
if (position != end)
return false;
bool ok;
int portInt = charactersToIntStrict(begin, end - begin, &ok);
if (portInt < 0 || portInt > std::numeric_limits<uint16_t>::max())
return false;
port = portInt;
return ok;
}
// 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).
static bool isNonceCharacter(UChar c)
{
return isBase64OrBase64URLCharacter(c) || c == '=';
}
// nonce-source = "'nonce-" nonce-value "'"
// nonce-value = base64-value
bool ContentSecurityPolicySourceList::parseNonceSource(const UChar* begin, const UChar* end)
{
const unsigned noncePrefixLength = 7;
if (!StringView(begin, end - begin).startsWithIgnoringASCIICase("'nonce-"))
return false;
const UChar* position = begin + noncePrefixLength;
const UChar* beginNonceValue = position;
skipWhile<UChar, isNonceCharacter>(position, end);
if (position >= end || position == beginNonceValue || *position != '\'')
return false;
m_nonces.add(String(beginNonceValue, position - beginNonceValue));
return true;
}
// hash-source = "'" hash-algorithm "-" base64-value "'"
// hash-algorithm = "sha256" / "sha384" / "sha512"
// base64-value = 1*( ALPHA / DIGIT / "+" / "/" / "-" / "_" )*2( "=" )
bool ContentSecurityPolicySourceList::parseHashSource(const UChar* begin, const UChar* end)
{
if (begin == end)
return false;
const UChar* position = begin;
if (!skipExactly<UChar>(position, end, '\''))
return false;
auto digest = parseCryptographicDigest(position, end);
if (!digest)
return false;
if (position >= end || *position != '\'')
return false;
if (digest->value.size() > ContentSecurityPolicyHash::maximumDigestLength)
return false;
m_hashAlgorithmsUsed.add(digest->algorithm);
m_hashes.add(WTFMove(*digest));
return true;
}
} // namespace WebCore