blob: 57117c831ef936af35c545d8b9600874446772bd [file] [log] [blame]
// Copyright 2015 The Cobalt Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "cobalt/csp/source_list.h"
#include <algorithm>
#include "base/base64.h"
#include "base/strings/string_number_conversions.h"
#include "cobalt/network/socket_address_parser.h"
#include "net/base/escape.h"
#include "starboard/common/socket.h"
#include "url/url_canon_ip.h"
#include "url/url_constants.h"
namespace cobalt {
namespace csp {
namespace {
bool IsSourceListNone(const char* begin, const char* end) {
SkipWhile<base::IsAsciiWhitespace>(&begin, end);
const char* position = begin;
SkipWhile<IsSourceCharacter>(&position, end);
size_t len = static_cast<size_t>(position - begin);
if (base::strncasecmp("'none'", begin, len) != 0) {
return false;
}
SkipWhile<base::IsAsciiWhitespace>(&position, end);
if (position != end) {
return false;
}
return true;
}
struct HashPrefix {
const char* prefix;
HashAlgorithm type;
};
HashPrefix kSupportedPrefixes[] = {
{"'sha256-", kHashAlgorithmSha256}, {"'sha384-", kHashAlgorithmSha384},
{"'sha512-", kHashAlgorithmSha512}, {"'sha-256-", kHashAlgorithmSha256},
{"'sha-384-", kHashAlgorithmSha384}, {"'sha-512-", kHashAlgorithmSha512},
};
bool SchemeCanMatchStar(const GURL& url) {
return !(url.SchemeIs("blob") || url.SchemeIs("data") ||
url.SchemeIs("filesystem"));
}
bool IsInsecureScheme(const GURL& url) {
return url.SchemeIs(url::kHttpScheme) || url.SchemeIs(url::kWsScheme);
}
} // namespace
SourceList::SourceList(const LocalNetworkCheckerInterface* checker,
ContentSecurityPolicy* policy,
const std::string& directive_name)
: policy_(policy),
directive_name_(directive_name),
allow_self_(false),
allow_star_(false),
allow_inline_(false),
allow_eval_(false),
allow_insecure_connections_to_local_network_(false),
allow_insecure_connections_to_localhost_(false),
allow_insecure_connections_to_private_range_(false),
hash_algorithms_used_(0),
local_network_checker_(checker) {}
bool SourceList::Matches(
const GURL& url,
ContentSecurityPolicy::RedirectStatus redirect_status) const {
if (allow_star_ && SchemeCanMatchStar(url)) {
return true;
}
if (allow_self_ && policy_->UrlMatchesSelf(url)) {
return true;
}
for (size_t i = 0; i < list_.size(); ++i) {
if (list_[i].Matches(url, redirect_status)) {
return true;
}
}
if (!url.is_valid() || url.is_empty()) {
return false;
}
// Since url is valid, we can now use possibly_invalid_spec() below.
const std::string& valid_spec = url.possibly_invalid_spec();
const url::Parsed& parsed = url.parsed_for_possibly_invalid_spec();
if (!url.has_host() || !parsed.host.is_valid() ||
!parsed.host.is_nonempty()) {
return false;
}
if (!IsInsecureScheme(url)) {
return false;
}
if (allow_insecure_connections_to_localhost_) {
std::string host;
// This will be our host string if we are not using IPV6.
host.append(valid_spec.c_str() + parsed.host.begin,
valid_spec.c_str() + parsed.host.begin + parsed.host.len);
#if SB_API_VERSION >= 12 || SB_HAS(IPV6)
#if SB_API_VERSION >= 12
if (SbSocketIsIpv6Supported())
#endif
host = url.HostNoBrackets();
#endif
if (net::HostStringIsLocalhost(host)) {
return true;
}
}
/* allow mixed content for local network or private ranges? */
if (!(allow_insecure_connections_to_local_network_ ||
allow_insecure_connections_to_private_range_)) {
return false;
}
SbSocketAddress destination;
// Note that GetDestinationForHost will only pass if host is a numeric IP.
if (!cobalt::network::ParseSocketAddress(valid_spec.c_str(), parsed.host,
&destination)) {
return false;
}
if (allow_insecure_connections_to_private_range_ &&
local_network_checker_->IsIPInPrivateRange(destination)) {
return true;
}
if (allow_insecure_connections_to_local_network_ &&
local_network_checker_->IsIPInLocalNetwork(destination)) {
return true;
}
return false;
}
bool SourceList::AllowInline() const { return allow_inline_; }
bool SourceList::AllowEval() const { return allow_eval_; }
bool SourceList::AllowNonce(const std::string& nonce) const {
return !nonce.empty() && nonces_.find(nonce) != nonces_.end();
}
bool SourceList::AllowHash(const HashValue& hash_value) const {
return hashes_.find(hash_value) != hashes_.end();
}
bool SourceList::hash_or_nonce_present() const {
return nonces_.size() > 0 || hash_algorithms_used_ != kHashAlgorithmNone;
}
// source-list = *WSP [ source *( 1*WSP source ) *WSP ]
// / *WSP "'none'" *WSP
//
void SourceList::Parse(const base::StringPiece& str) {
// We represent 'none' as an empty list_.
const char* begin = str.begin();
const char* end = str.end();
if (IsSourceListNone(begin, end)) {
return;
}
const char* position = begin;
while (position < end) {
SkipWhile<base::IsAsciiWhitespace>(&position, end);
if (position == end) {
return;
}
const char* begin_source = position;
SkipWhile<IsSourceCharacter>(&position, end);
SourceConfig config;
if (ParseSource(begin_source, position, &config)) {
// Wildcard hosts and keyword sources ('self', 'unsafe-inline',
// etc.) aren't stored in m_list, but as attributes on the source
// list itself.
if (config.scheme.empty() && config.host.empty()) {
continue;
}
if (policy_->IsDirectiveName(config.host)) {
policy_->ReportDirectiveAsSourceExpression(directive_name_,
config.host);
}
list_.push_back(Source(policy_, config));
} else {
policy_->ReportInvalidSourceExpression(directive_name_,
ToString(begin_source, position));
}
DCHECK(position == end || base::IsAsciiWhitespace(*position));
}
}
// source = scheme ":"
// / ( [ scheme "://" ] host [ port ] [ path ] )
// / "'self'"
bool SourceList::ParseSource(const char* begin, const char* end,
SourceConfig* config) {
if (begin == end) {
return false;
}
if (base::LowerCaseEqualsASCII(begin, end, "'none'")) {
return false;
}
base::StringPiece begin_piece(begin, static_cast<size_t>(end - begin));
if (begin_piece.length() == 1 && begin_piece[0] == '*') {
AddSourceStar();
return true;
}
if (base::LowerCaseEqualsASCII(begin, end, "'self'")) {
AddSourceSelf();
return true;
}
if (base::LowerCaseEqualsASCII(begin, end, "'unsafe-inline'")) {
AddSourceUnsafeInline();
return true;
}
if (base::LowerCaseEqualsASCII(begin, end, "'unsafe-eval'")) {
AddSourceUnsafeEval();
return true;
}
if (directive_name_.compare(ContentSecurityPolicy::kConnectSrc) == 0) {
if (base::LowerCaseEqualsASCII(begin, end, "'cobalt-insecure-localhost'")) {
AddSourceLocalhost();
return true;
}
if (base::LowerCaseEqualsASCII(begin, end,
"'cobalt-insecure-local-network'")) {
AddSourceLocalNetwork();
return true;
}
if (base::LowerCaseEqualsASCII(begin, end,
"'cobalt-insecure-private-range'")) {
AddSourcePrivateRange();
return true;
}
}
std::string nonce;
if (!ParseNonce(begin, end, &nonce)) {
return false;
}
if (!nonce.empty()) {
AddSourceNonce(nonce);
return true;
}
DigestValue hash;
HashAlgorithm algorithm = kHashAlgorithmNone;
if (!ParseHash(begin, end, &hash, &algorithm)) {
return false;
}
if (hash.size() > 0) {
AddSourceHash(algorithm, hash);
return true;
}
const char* position = begin;
const char* begin_host = begin;
const char* begin_path = end;
const char* begin_port = 0;
SkipWhile<IsNotColonOrSlash>(&position, end);
if (position == end) {
// host
// ^
return ParseHost(begin_host, position, &config->host,
&config->host_wildcard);
}
if (position < end && *position == '/') {
// host/path || host/ || /
// ^ ^ ^
return ParseHost(begin_host, position, &config->host,
&config->host_wildcard) &&
ParsePath(position, end, &config->path);
}
if (position < end && *position == ':') {
if (end - position == 1) {
// scheme:
// ^
return ParseScheme(begin, position, &config->scheme);
}
if (position[1] == '/') {
// scheme://host || scheme://
// ^ ^
if (!ParseScheme(begin, position, &config->scheme) ||
!SkipExactly(&position, end, ':') ||
!SkipExactly(&position, end, '/') ||
!SkipExactly(&position, end, '/')) {
return false;
}
if (position == end) {
return false;
}
begin_host = position;
SkipWhile<IsNotColonOrSlash>(&position, end);
}
if (position < end && *position == ':') {
// host:port || scheme://host:port
// ^ ^
begin_port = position;
SkipUntil(&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,
&config->host, &config->host_wildcard)) {
return false;
}
if (begin_port) {
if (!ParsePort(begin_port, begin_path, &config->port,
&config->port_wildcard)) {
return false;
}
} else {
config->port = url::PORT_UNSPECIFIED;
}
if (begin_path != end) {
if (!ParsePath(begin_path, end, &config->path)) {
return false;
}
}
return true;
}
// nonce-source = "'nonce-" nonce-value "'"
// nonce-value = 1*( ALPHA / DIGIT / "+" / "/" / "=" )
//
bool SourceList::ParseNonce(const char* begin, const char* end,
std::string* nonce) {
size_t nonce_length = static_cast<size_t>(end - begin);
const char* prefix = "'nonce-";
if (nonce_length <= strlen(prefix) ||
base::strncasecmp(prefix, begin, strlen(prefix)) != 0) {
return true;
}
const char* position = begin + strlen(prefix);
const char* nonce_begin = position;
DCHECK_LT(position, end);
SkipWhile<IsNonceCharacter>(&position, end);
DCHECK_LE(nonce_begin, position);
if (position + 1 != end || *position != '\'' || position == nonce_begin) {
return false;
}
*nonce = ToString(nonce_begin, position);
return true;
}
// hash-source = "'" hash-algorithm "-" hash-value "'"
// hash-algorithm = "sha1" / "sha256" / "sha384" / "sha512"
// hash-value = 1*( ALPHA / DIGIT / "+" / "/" / "=" )
//
// static
bool SourceList::ParseHash(const char* begin, const char* end,
DigestValue* hash, HashAlgorithm* hash_algorithm) {
std::string prefix;
*hash_algorithm = kHashAlgorithmNone;
size_t hash_length = static_cast<size_t>(end - begin);
for (size_t i = 0; i < arraysize(kSupportedPrefixes); ++i) {
const HashPrefix& algorithm = kSupportedPrefixes[i];
if (hash_length > strlen(algorithm.prefix) &&
base::strncasecmp(algorithm.prefix, begin, strlen(algorithm.prefix)) ==
0) {
prefix = algorithm.prefix;
*hash_algorithm = algorithm.type;
break;
}
}
if (*hash_algorithm == kHashAlgorithmNone) {
return true;
}
const char* position = begin + prefix.length();
const char* hash_begin = position;
DCHECK_LT(position, end);
SkipWhile<IsBase64EncodedCharacter>(&position, end);
DCHECK_LE(hash_begin, position);
// Base64 encodings may end with exactly one or two '=' characters
if (position < end) {
SkipExactly(&position, position + 1, '=');
}
if (position < end) {
SkipExactly(&position, position + 1, '=');
}
if (position + 1 != end || *position != '\'' || position == hash_begin) {
return false;
}
// We accept base64url-encoded data here by normalizing it to base64.
// TODO: Blink has a NormalizeToBase64() step here.
std::string hash_vector;
base::Base64Decode(
base::StringPiece(hash_begin, static_cast<size_t>(position - hash_begin)),
&hash_vector);
if (hash_vector.size() > kMaxDigestSize) {
return false;
}
hash->assign(hash_vector.begin(), hash_vector.end());
return true;
}
// ; <scheme> production from RFC 3986
// scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
//
bool SourceList::ParseScheme(const char* begin, const char* end,
std::string* scheme) {
DCHECK_LE(begin, end);
DCHECK(scheme && scheme->empty());
if (begin == end) {
return false;
}
const char* position = begin;
if (!SkipExactly<base::IsAsciiAlpha>(&position, end)) {
return false;
}
SkipWhile<IsSchemeContinuationCharacter>(&position, end);
if (position != end) {
return false;
}
*scheme = ToString(begin, end);
return true;
}
// host = [ "*." ] 1*host-char *( "." 1*host-char )
// / "*"
// host-char = ALPHA / DIGIT / "-"
//
bool SourceList::ParseHost(const char* begin, const char* end,
std::string* host,
SourceConfig::WildcardDisposition* host_wildcard) {
DCHECK_LE(begin, end);
DCHECK(host && host->empty());
DCHECK(host_wildcard && *host_wildcard == SourceConfig::kNoWildcard);
if (begin == end) {
return false;
}
const char* position = begin;
if (SkipExactly(&position, end, '*')) {
*host_wildcard = SourceConfig::kHasWildcard;
if (position == end) {
return true;
}
if (!SkipExactly(&position, end, '.')) {
return false;
}
}
const char* host_begin = position;
while (position < end) {
if (!SkipExactly<IsHostCharacter>(&position, end)) {
return false;
}
SkipWhile<IsHostCharacter>(&position, end);
if (position < end && !SkipExactly(&position, end, '.')) {
return false;
}
}
DCHECK_EQ(position, end);
*host = ToString(host_begin, end);
return true;
}
bool SourceList::ParsePath(const char* begin, const char* end,
std::string* path) {
DCHECK_LE(begin, end);
DCHECK(path && path->empty());
const char* position = begin;
SkipWhile<IsPathComponentCharacter>(&position, end);
// path/to/file.js?query=string || path/to/file.js#anchor
// ^ ^
if (position < end) {
policy_->ReportInvalidPathCharacter(directive_name_, ToString(begin, end),
*position);
}
*path = net::UnescapeURLComponent(
ToString(begin, position),
net::UnescapeRule::SPACES | net::UnescapeRule::PATH_SEPARATORS |
net::UnescapeRule::URL_SPECIAL_CHARS_EXCEPT_PATH_SEPARATORS);
DCHECK_LE(position, end);
DCHECK(position == end || (*position == '#' || *position == '?'));
return true;
}
// port = ":" ( 1*DIGIT / "*" )
//
bool SourceList::ParsePort(const char* begin, const char* end, int* port,
SourceConfig::WildcardDisposition* port_wildcard) {
DCHECK_LE(begin, end);
DCHECK(port && *port == url::PORT_UNSPECIFIED);
DCHECK(port_wildcard && *port_wildcard == SourceConfig::kNoWildcard);
if (!SkipExactly(&begin, end, ':')) {
NOTREACHED();
}
if (begin == end) {
return false;
}
if (end - begin == 1 && *begin == '*') {
*port = url::PORT_UNSPECIFIED;
*port_wildcard = SourceConfig::kHasWildcard;
return true;
}
const char* position = begin;
SkipWhile<base::IsAsciiDigit>(&position, end);
if (position != end) {
return false;
}
bool ok = base::StringToInt(
base::StringPiece(begin, static_cast<size_t>(end - begin)), port);
return ok;
}
void SourceList::AddSourceLocalhost() {
allow_insecure_connections_to_localhost_ = true;
}
void SourceList::AddSourceLocalNetwork() {
allow_insecure_connections_to_local_network_ = true;
}
void SourceList::AddSourcePrivateRange() {
allow_insecure_connections_to_private_range_ = true;
}
void SourceList::AddSourceSelf() { allow_self_ = true; }
void SourceList::AddSourceStar() { allow_star_ = true; }
void SourceList::AddSourceUnsafeInline() { allow_inline_ = true; }
void SourceList::AddSourceUnsafeEval() { allow_eval_ = true; }
void SourceList::AddSourceNonce(const std::string& nonce) {
nonces_.insert(nonce);
}
void SourceList::AddSourceHash(const HashAlgorithm& algorithm,
const DigestValue& hash) {
hashes_.insert(HashValue(algorithm, hash));
hash_algorithms_used_ |= algorithm;
}
} // namespace csp
} // namespace cobalt