ContentSecurityPolicySourceList.cpp [plain text]
#include "config.h"
#include "ContentSecurityPolicySourceList.h"
#include "ContentSecurityPolicy.h"
#include "ContentSecurityPolicyDirectiveNames.h"
#include "ParsingUtilities.h"
#include "SecurityOrigin.h"
#include "URL.h"
#include <wtf/ASCIICType.h>
#include <wtf/NeverDestroyed.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;
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)
{
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);
}
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;
std::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)) {
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));
}
}
bool ContentSecurityPolicySourceList::parseSource(const UChar* begin, const UChar* end, String& scheme, String& host, std::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) {
return parseHost(beginHost, position, host, hostHasWildcard);
}
if (position < end && *position == '/') {
return parseHost(beginHost, position, host, hostHasWildcard) && parsePath(position, end, path);
}
if (position < end && *position == ':') {
if (end - position == 1) {
return parseScheme(begin, position, scheme);
}
if (position[1] == '/') {
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 == ':') {
beginPort = position;
skipUntil<UChar>(position, end, '/');
}
}
if (position < end && *position == '/') {
if (position == beginHost)
return false;
beginPath = position;
}
if (!parseHost(beginHost, beginPort ? beginPort : beginPath, host, hostHasWildcard))
return false;
if (!beginPort)
port = std::nullopt;
else {
if (!parsePort(beginPort, beginPath, port, portHasWildcard))
return false;
}
if (beginPath != end) {
if (!parsePath(beginPath, end, path))
return false;
}
return true;
}
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;
}
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);
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;
}
bool ContentSecurityPolicySourceList::parsePort(const UChar* begin, const UChar* end, std::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 = std::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;
}
static bool isBase64Character(UChar c)
{
return isASCIIAlphanumeric(c) || c == '+' || c == '/' || c == '-' || c == '_';
}
static bool isNonceCharacter(UChar c)
{
return isBase64Character(c) || c == '=';
}
bool ContentSecurityPolicySourceList::parseNonceSource(const UChar* begin, const UChar* end)
{
static NeverDestroyed<String> noncePrefix("'nonce-", String::ConstructFromLiteral);
if (!StringView(begin, end - begin).startsWithIgnoringASCIICase(noncePrefix.get()))
return false;
const UChar* position = begin + noncePrefix.get().length();
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;
}
static bool parseHashAlgorithmAdvancingPosition(const UChar*& position, size_t length, ContentSecurityPolicyHashAlgorithm& algorithm)
{
static struct {
NeverDestroyed<String> label;
ContentSecurityPolicyHashAlgorithm algorithm;
} labelToHashAlgorithmTable[] {
{ ASCIILiteral("sha256"), ContentSecurityPolicyHashAlgorithm::SHA_256 },
{ ASCIILiteral("sha384"), ContentSecurityPolicyHashAlgorithm::SHA_384 },
{ ASCIILiteral("sha512"), ContentSecurityPolicyHashAlgorithm::SHA_512 },
};
StringView stringView(position, length);
for (auto& entry : labelToHashAlgorithmTable) {
String& label = entry.label.get();
if (!stringView.startsWithIgnoringASCIICase(label))
continue;
position += label.length();
algorithm = entry.algorithm;
return true;
}
return false;
}
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;
ContentSecurityPolicyHashAlgorithm algorithm;
if (!parseHashAlgorithmAdvancingPosition(position, end - position, algorithm))
return false;
if (!skipExactly<UChar>(position, end, '-'))
return false;
const UChar* beginHashValue = position;
skipWhile<UChar, isBase64Character>(position, end);
skipExactly<UChar>(position, end, '=');
skipExactly<UChar>(position, end, '=');
if (position >= end || position == beginHashValue || *position != '\'')
return false;
Vector<uint8_t> digest;
StringView hashValue(beginHashValue, position - beginHashValue); if (!base64Decode(hashValue.toStringWithoutCopying(), digest, Base64ValidatePadding)) {
if (!base64URLDecode(hashValue.toStringWithoutCopying(), digest))
return false;
}
if (digest.size() > maximumContentSecurityPolicyDigestLength)
return false;
m_hashes.add(std::make_pair(algorithm, digest));
m_hashAlgorithmsUsed |= algorithm;
return true;
}
}