blob: 9c885b7366c11e6ae7e31fd6e4a1c44551d10386 [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 "core/frame/csp/CSPSourceList.h"
#include "core/frame/csp/CSPSource.h"
#include "core/frame/csp/ContentSecurityPolicy.h"
#include "platform/ParsingUtilities.h"
#include "platform/weborigin/KURL.h"
#include "platform/weborigin/SecurityOrigin.h"
#include "wtf/HashSet.h"
#include "wtf/text/Base64.h"
#include "wtf/text/StringToNumber.h"
#include "wtf/text/WTFString.h"
namespace blink {
static bool isSourceListNone(const UChar* begin, const UChar* end)
{
skipWhile<UChar, isASCIISpace>(begin, end);
const UChar* position = begin;
skipWhile<UChar, isSourceCharacter>(position, end);
if (!equalIgnoringCase("'none'", begin, position - begin))
return false;
skipWhile<UChar, isASCIISpace>(position, end);
if (position != end)
return false;
return true;
}
CSPSourceList::CSPSourceList(ContentSecurityPolicy* policy, const String& directiveName)
: m_policy(policy)
, m_directiveName(directiveName)
, m_allowSelf(false)
, m_allowStar(false)
, m_allowInline(false)
, m_allowEval(false)
, m_allowDynamic(false)
, m_allowHashedAttributes(false)
, m_hashAlgorithmsUsed(0)
{
}
bool CSPSourceList::matches(const KURL& url, ResourceRequest::RedirectStatus redirectStatus) const
{
// The CSP spec specifically states that data:, blob:, and filesystem URLs
// should not be captured by a '*" source
// (http://www.w3.org/TR/CSP2/#source-list-guid-matching). Thus, in the
// case of a full wildcard, data:, blob:, and filesystem: URLs are
// explicitly checked for in the source list before allowing them through.
if (m_allowStar) {
if (url.protocolIs("blob") || url.protocolIs("data") || url.protocolIs("filesystem"))
return hasSourceMatchInList(url, redirectStatus);
return true;
}
KURL effectiveURL = m_policy->selfMatchesInnerURL() && SecurityOrigin::shouldUseInnerURL(url) ? SecurityOrigin::extractInnerURL(url) : url;
if (m_allowSelf && m_policy->urlMatchesSelf(effectiveURL))
return true;
return hasSourceMatchInList(effectiveURL, redirectStatus);
}
bool CSPSourceList::allowInline() const
{
return m_allowInline;
}
bool CSPSourceList::allowEval() const
{
return m_allowEval;
}
bool CSPSourceList::allowDynamic() const
{
return m_allowDynamic;
}
bool CSPSourceList::allowNonce(const String& nonce) const
{
return !nonce.isNull() && m_nonces.contains(nonce);
}
bool CSPSourceList::allowHash(const CSPHashValue& hashValue) const
{
return m_hashes.contains(hashValue);
}
bool CSPSourceList::allowHashedAttributes() const
{
return m_allowHashedAttributes;
}
uint8_t CSPSourceList::hashAlgorithmsUsed() const
{
return m_hashAlgorithmsUsed;
}
bool CSPSourceList::isHashOrNoncePresent() const
{
return !m_nonces.isEmpty() || m_hashAlgorithmsUsed != ContentSecurityPolicyHashAlgorithmNone;
}
// source-list = *WSP [ source *( 1*WSP source ) *WSP ]
// / *WSP "'none'" *WSP
//
void CSPSourceList::parse(const UChar* begin, const UChar* end)
{
// We represent 'none' as an empty m_list.
if (isSourceListNone(begin, end))
return;
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;
int port = 0;
CSPSource::WildcardDisposition hostWildcard = CSPSource::NoWildcard;
CSPSource::WildcardDisposition portWildcard = CSPSource::NoWildcard;
if (parseSource(beginSource, position, scheme, host, port, path, hostWildcard, portWildcard)) {
// 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 (m_policy->isDirectiveName(host))
m_policy->reportDirectiveAsSourceExpression(m_directiveName, host);
m_list.append(new CSPSource(m_policy, scheme, host, port, path, hostWildcard, portWildcard));
} else {
m_policy->reportInvalidSourceExpression(m_directiveName, String(beginSource, position - beginSource));
}
ASSERT(position == end || isASCIISpace(*position));
}
}
// source = scheme ":"
// / ( [ scheme "://" ] host [ port ] [ path ] )
// / "'self'"
bool CSPSourceList::parseSource(const UChar* begin, const UChar* end, String& scheme, String& host, int& port, String& path, CSPSource::WildcardDisposition& hostWildcard, CSPSource::WildcardDisposition& portWildcard)
{
if (begin == end)
return false;
if (equalIgnoringCase("'none'", begin, end - begin))
return false;
if (end - begin == 1 && *begin == '*') {
addSourceStar();
return true;
}
if (equalIgnoringCase("'self'", begin, end - begin)) {
addSourceSelf();
return true;
}
if (equalIgnoringCase("'unsafe-inline'", begin, end - begin)) {
addSourceUnsafeInline();
return true;
}
if (equalIgnoringCase("'unsafe-eval'", begin, end - begin)) {
addSourceUnsafeEval();
return true;
}
if (equalIgnoringCase("'strict-dynamic'", begin, end - begin)) {
addSourceStrictDynamic();
return true;
}
if (equalIgnoringCase("'unsafe-hashed-attributes'", begin, end - begin)) {
addSourceUnsafeHashedAttributes();
return true;
}
String nonce;
if (!parseNonce(begin, end, nonce))
return false;
if (!nonce.isNull()) {
addSourceNonce(nonce);
return true;
}
DigestValue hash;
ContentSecurityPolicyHashAlgorithm algorithm = ContentSecurityPolicyHashAlgorithmNone;
if (!parseHash(begin, end, hash, algorithm))
return false;
if (hash.size() > 0) {
addSourceHash(algorithm, hash);
return true;
}
const UChar* position = begin;
const UChar* beginHost = begin;
const UChar* beginPath = end;
const UChar* beginPort = 0;
skipWhile<UChar, isNotColonOrSlash>(position, end);
if (position == end) {
// host
// ^
return parseHost(beginHost, position, host, hostWildcard);
}
if (position < end && *position == '/') {
// host/path || host/ || /
// ^ ^ ^
return parseHost(beginHost, position, host, hostWildcard) && 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, hostWildcard))
return false;
if (beginPort) {
if (!parsePort(beginPort, beginPath, port, portWildcard))
return false;
} else {
port = 0;
}
if (beginPath != end) {
if (!parsePath(beginPath, end, path))
return false;
}
return true;
}
// nonce-source = "'nonce-" nonce-value "'"
// nonce-value = 1*( ALPHA / DIGIT / "+" / "/" / "=" )
//
bool CSPSourceList::parseNonce(const UChar* begin, const UChar* end, String& nonce)
{
size_t nonceLength = end - begin;
const char* prefix = "'nonce-";
if (nonceLength <= strlen(prefix) || !equalIgnoringCase(prefix, begin, strlen(prefix)))
return true;
const UChar* position = begin + strlen(prefix);
const UChar* nonceBegin = position;
ASSERT(position < end);
skipWhile<UChar, isNonceCharacter>(position, end);
ASSERT(nonceBegin <= position);
if (position + 1 != end || *position != '\'' || position == nonceBegin)
return false;
nonce = String(nonceBegin, position - nonceBegin);
return true;
}
// hash-source = "'" hash-algorithm "-" hash-value "'"
// hash-algorithm = "sha1" / "sha256" / "sha384" / "sha512"
// hash-value = 1*( ALPHA / DIGIT / "+" / "/" / "=" )
//
bool CSPSourceList::parseHash(const UChar* begin, const UChar* end, DigestValue& hash, ContentSecurityPolicyHashAlgorithm& hashAlgorithm)
{
// Any additions or subtractions from this struct should also modify the
// respective entries in the kAlgorithmMap array in checkDigest().
static const struct {
const char* prefix;
ContentSecurityPolicyHashAlgorithm type;
} kSupportedPrefixes[] = {
// FIXME: Drop support for SHA-1. It's not in the spec.
{ "'sha1-", ContentSecurityPolicyHashAlgorithmSha1 },
{ "'sha256-", ContentSecurityPolicyHashAlgorithmSha256 },
{ "'sha384-", ContentSecurityPolicyHashAlgorithmSha384 },
{ "'sha512-", ContentSecurityPolicyHashAlgorithmSha512 },
{ "'sha-256-", ContentSecurityPolicyHashAlgorithmSha256 },
{ "'sha-384-", ContentSecurityPolicyHashAlgorithmSha384 },
{ "'sha-512-", ContentSecurityPolicyHashAlgorithmSha512 }
};
String prefix;
hashAlgorithm = ContentSecurityPolicyHashAlgorithmNone;
size_t hashLength = end - begin;
for (const auto& algorithm : kSupportedPrefixes) {
if (hashLength > strlen(algorithm.prefix) && equalIgnoringCase(algorithm.prefix, begin, strlen(algorithm.prefix))) {
prefix = algorithm.prefix;
hashAlgorithm = algorithm.type;
break;
}
}
if (hashAlgorithm == ContentSecurityPolicyHashAlgorithmNone)
return true;
const UChar* position = begin + prefix.length();
const UChar* hashBegin = position;
ASSERT(position < end);
skipWhile<UChar, isBase64EncodedCharacter>(position, end);
ASSERT(hashBegin <= 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 == hashBegin)
return false;
Vector<char> hashVector;
// We accept base64url-encoded data here by normalizing it to base64.
base64Decode(normalizeToBase64(String(hashBegin, position - hashBegin)), hashVector);
if (hashVector.size() > kMaxDigestSize)
return false;
hash.append(reinterpret_cast<uint8_t*>(hashVector.data()), hashVector.size());
return true;
}
// ; <scheme> production from RFC 3986
// scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
//
bool CSPSourceList::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 CSPSourceList::parseHost(const UChar* begin, const UChar* end, String& host, CSPSource::WildcardDisposition& hostWildcard)
{
ASSERT(begin <= end);
ASSERT(host.isEmpty());
ASSERT(hostWildcard == CSPSource::NoWildcard);
if (begin == end)
return false;
const UChar* position = begin;
if (skipExactly<UChar>(position, end, '*')) {
hostWildcard = CSPSource::HasWildcard;
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 CSPSourceList::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 CSPSourceList::parsePort(const UChar* begin, const UChar* end, int& port, CSPSource::WildcardDisposition& portWildcard)
{
ASSERT(begin <= end);
ASSERT(!port);
ASSERT(portWildcard == CSPSource::NoWildcard);
if (!skipExactly<UChar>(begin, end, ':'))
ASSERT_NOT_REACHED();
if (begin == end)
return false;
if (end - begin == 1 && *begin == '*') {
port = 0;
portWildcard = CSPSource::HasWildcard;
return true;
}
const UChar* position = begin;
skipWhile<UChar, isASCIIDigit>(position, end);
if (position != end)
return false;
bool ok;
port = charactersToIntStrict(begin, end - begin, &ok);
return ok;
}
void CSPSourceList::addSourceSelf()
{
m_allowSelf = true;
}
void CSPSourceList::addSourceStar()
{
m_allowStar = true;
}
void CSPSourceList::addSourceUnsafeInline()
{
m_allowInline = true;
}
void CSPSourceList::addSourceUnsafeEval()
{
m_allowEval = true;
}
void CSPSourceList::addSourceStrictDynamic()
{
m_allowDynamic = true;
}
void CSPSourceList::addSourceUnsafeHashedAttributes()
{
m_allowHashedAttributes = true;
}
void CSPSourceList::addSourceNonce(const String& nonce)
{
m_nonces.add(nonce);
}
void CSPSourceList::addSourceHash(const ContentSecurityPolicyHashAlgorithm& algorithm, const DigestValue& hash)
{
m_hashes.add(CSPHashValue(algorithm, hash));
m_hashAlgorithmsUsed |= algorithm;
}
bool CSPSourceList::hasSourceMatchInList(const KURL& url, ResourceRequest::RedirectStatus redirectStatus) const
{
for (size_t i = 0; i < m_list.size(); ++i) {
if (m_list[i]->matches(url, redirectStatus))
return true;
}
return false;
}
DEFINE_TRACE(CSPSourceList)
{
visitor->trace(m_policy);
visitor->trace(m_list);
}
} // namespace blink