blob: f43aa78880bc10aad02d9c373a038b6d4455cf6d [file] [log] [blame]
/*
* Copyright 2015 Google Inc. 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/content_security_policy.h"
#include "base/string_util.h"
#include "base/values.h"
#include "cobalt/csp/directive_list.h"
#include "cobalt/csp/source.h"
namespace cobalt {
namespace csp {
namespace {
ReferrerPolicy MergeReferrerPolicies(ReferrerPolicy a, ReferrerPolicy b) {
// If there are conflicting policies, err on the side of security, i.e.
// send no referrer. This could happen if there is a CSP policy in the
// response headers from the server and then a different one in a <meta>
// in the document.
if (a != b) {
return kReferrerPolicyNoReferrer;
} else {
return a;
}
}
// Macros for iterating over the CSP's policies_ list and
// calling a given function.
#define FOR_ALL_POLICIES_1(AllowFunc, arg0) \
for (PolicyList::const_iterator it = policies_.begin(); \
it != policies_.end(); ++it) { \
if (!(*it)->AllowFunc((arg0))) return false; \
} \
return true
#define FOR_ALL_POLICIES_3(AllowFunc, arg0, arg1, arg2) \
for (PolicyList::const_iterator it = policies_.begin(); \
it != policies_.end(); ++it) { \
if ((*it)->AllowFunc((arg0), (arg1), (arg2)) == false) return false; \
} \
return true
#define FOR_ALL_POLICIES_4(AllowFunc, arg0, arg1, arg2, arg3) \
for (PolicyList::const_iterator it = policies_.begin(); \
it != policies_.end(); ++it) { \
if ((*it)->AllowFunc((arg0), (arg1), (arg2), (arg3)) == false) { \
return false; \
} \
} \
return true
// Similar to above but templatized on a member variable.
// Used by CheckDigest().
template <bool (DirectiveList::*allowed)(const HashValue&) const>
bool IsAllowedByAllWithHash(const ContentSecurityPolicy::PolicyList& policies,
const HashValue& hash_value) {
for (ContentSecurityPolicy::PolicyList::const_iterator it = policies.begin();
it != policies.end(); ++it) {
if (!(*it->*allowed)(hash_value)) {
return false;
}
}
return true;
}
template <bool (DirectiveList::*allowed)(const HashValue&) const>
bool CheckDigest(const std::string& source, uint8 hash_algorithms_used,
const ContentSecurityPolicy::PolicyList& policies) {
if (hash_algorithms_used == kHashAlgorithmNone) {
return false;
}
HashAlgorithm valid_hash_algorithms[] = {
kHashAlgorithmSha256, kHashAlgorithmSha384, kHashAlgorithmSha512,
};
for (size_t i = 0; i < arraysize(valid_hash_algorithms); ++i) {
DigestValue digest;
if (valid_hash_algorithms[i] & hash_algorithms_used) {
bool digest_success = ComputeDigest(
valid_hash_algorithms[i], source.c_str(), source.length(), &digest);
if (digest_success &&
IsAllowedByAllWithHash<allowed>(
policies, HashValue(valid_hash_algorithms[i], digest))) {
return true;
}
}
}
return false;
}
} // namespace
ResponseHeaders::ResponseHeaders(
const scoped_refptr<net::HttpResponseHeaders>& response) {
response->GetNormalizedHeader("Content-Security-Policy",
&content_security_policy_);
response->GetNormalizedHeader("Content-Security-Policy-Report-Only",
&content_security_policy_report_only_);
}
// CSP Level 1 Directives
const char ContentSecurityPolicy::kConnectSrc[] = "connect-src";
const char ContentSecurityPolicy::kDefaultSrc[] = "default-src";
const char ContentSecurityPolicy::kFontSrc[] = "font-src";
const char ContentSecurityPolicy::kFrameSrc[] = "frame-src";
const char ContentSecurityPolicy::kImgSrc[] = "img-src";
const char ContentSecurityPolicy::kMediaSrc[] = "media-src";
const char ContentSecurityPolicy::kObjectSrc[] = "object-src";
const char ContentSecurityPolicy::kReportURI[] = "report-uri";
const char ContentSecurityPolicy::kSandbox[] = "sandbox";
const char ContentSecurityPolicy::kScriptSrc[] = "script-src";
const char ContentSecurityPolicy::kStyleSrc[] = "style-src";
// CSP Level 2 Directives
const char ContentSecurityPolicy::kBaseURI[] = "base-uri";
const char ContentSecurityPolicy::kChildSrc[] = "child-src";
const char ContentSecurityPolicy::kFormAction[] = "form-action";
const char ContentSecurityPolicy::kFrameAncestors[] = "frame-ancestors";
const char ContentSecurityPolicy::kPluginTypes[] = "plugin-types";
const char ContentSecurityPolicy::kReflectedXSS[] = "reflected-xss";
const char ContentSecurityPolicy::kReferrer[] = "referrer";
// Custom Cobalt directive to enforce navigation restrictions.
const char ContentSecurityPolicy::kLocationSrc[] = "h5vcc-location-src";
// CSP Editor's Draft:
// https://w3c.github.io/webappsec/specs/content-security-policy
const char ContentSecurityPolicy::kManifestSrc[] = "manifest-src";
// Mixed Content Directive
// https://w3c.github.io/webappsec/specs/mixedcontent/#strict-mode
const char ContentSecurityPolicy::kBlockAllMixedContent[] =
"block-all-mixed-content";
// https://w3c.github.io/webappsec/specs/upgrade/
const char ContentSecurityPolicy::kUpgradeInsecureRequests[] =
"upgrade-insecure-requests";
// Suborigin Directive
// https://metromoxie.github.io/webappsec/specs/suborigins/index.html
const char ContentSecurityPolicy::kSuborigin[] = "suborigin";
// clang-format off
bool ContentSecurityPolicy::IsDirectiveName(const std::string& name) {
std::string lower_name = StringToLowerASCII(name);
return (lower_name == kConnectSrc ||
lower_name == kDefaultSrc ||
lower_name == kFontSrc ||
lower_name == kFrameSrc ||
lower_name == kImgSrc ||
lower_name == kLocationSrc ||
lower_name == kMediaSrc ||
lower_name == kObjectSrc ||
lower_name == kReportURI ||
lower_name == kSandbox ||
lower_name == kSuborigin ||
lower_name == kScriptSrc ||
lower_name == kStyleSrc ||
lower_name == kBaseURI ||
lower_name == kChildSrc ||
lower_name == kFormAction ||
lower_name == kFrameAncestors ||
lower_name == kPluginTypes ||
lower_name == kReflectedXSS ||
lower_name == kReferrer ||
lower_name == kManifestSrc ||
lower_name == kBlockAllMixedContent ||
lower_name == kUpgradeInsecureRequests);
}
// clang-format on
ContentSecurityPolicy::ContentSecurityPolicy(
const GURL& url, const ViolationCallback& violation_callback)
: violation_callback_(violation_callback),
script_hash_algorithms_used_(0),
style_hash_algorithms_used_(0),
enforce_strict_mixed_content_checking_(false),
referrer_policy_(kReferrerPolicyDefault) {
NotifyUrlChanged(url);
}
ContentSecurityPolicy::~ContentSecurityPolicy() {}
void ContentSecurityPolicy::OnReceiveHeaders(const ResponseHeaders& headers) {
if (!headers.content_security_policy().empty()) {
AddPolicyFromHeaderValue(headers.content_security_policy(),
kHeaderTypeEnforce, kHeaderSourceHTTP);
}
if (!headers.content_security_policy_report_only().empty()) {
AddPolicyFromHeaderValue(headers.content_security_policy_report_only(),
kHeaderTypeReport, kHeaderSourceHTTP);
}
}
void ContentSecurityPolicy::OnReceiveHeader(const std::string& header,
HeaderType type,
HeaderSource source) {
AddPolicyFromHeaderValue(header, type, source);
}
void ContentSecurityPolicy::SetNavigationPolicy(const std::string& policy) {
navigation_policy_.reset(new DirectiveList(
this, base::StringPiece(policy), kHeaderTypeEnforce, kHeaderSourceHTTP));
}
bool ContentSecurityPolicy::UrlMatchesSelf(const GURL& url) const {
return self_source_->Matches(url, kDidNotRedirect);
}
bool ContentSecurityPolicy::SchemeMatchesSelf(const GURL& url) const {
// https://www.w3.org/TR/CSP2/#match-source-expression, section 4.5.1
// Allow "upgrade" to https if our document is http.
if (LowerCaseEqualsASCII(self_scheme_, "http")) {
return url.SchemeIs("http") || url.SchemeIs("https");
} else {
return self_scheme_ == url.scheme();
}
}
void ContentSecurityPolicy::ReportViolation(
const std::string& directive_text, const std::string& effective_directive,
const std::string& console_message, const GURL& blocked_url,
const std::vector<std::string>& report_endpoints,
const std::string& header) {
if (violation_callback_.is_null()) {
return;
}
ViolationInfo violation_info;
violation_info.directive_text = directive_text;
violation_info.effective_directive = effective_directive;
violation_info.console_message = console_message;
violation_info.blocked_url = blocked_url;
violation_info.endpoints = report_endpoints;
violation_info.header = header;
violation_callback_.Run(violation_info);
}
void ContentSecurityPolicy::ReportInvalidReferrer(
const std::string& invalid_value) {
std::string message =
"The 'referrer' Content Security Policy directive has the invalid value "
"\"" +
invalid_value +
"\". Valid values are \"no-referrer\", \"no-referrer-when-downgrade\", "
"\"origin\", \"origin-when-cross-origin\", and \"unsafe-url\".";
DLOG(WARNING) << message;
}
void ContentSecurityPolicy::ReportInvalidPluginTypes(
const std::string& plugin_type) {
std::string message;
if (plugin_type.empty()) {
message =
"'plugin-types' Content Security Policy directive is empty; all "
"plugins will be blocked.\n";
} else if (plugin_type == "'none'") {
message =
"Invalid plugin type in 'plugin-types' Content Security Policy "
"directive: '";
message += plugin_type;
message += "'. Did you mean to set the object-src directive to 'none'?\n";
} else {
message =
"Invalid plugin type in 'plugin-types' Content Security Policy "
"directive: '";
message += plugin_type;
message += "'.\n";
}
DLOG(WARNING) << message;
}
void ContentSecurityPolicy::ReportMetaOutsideHead(const std::string& header) {
std::string message = "The Content Security Policy '" + header +
"' was delivered via a <meta> element outside the "
"document's <head>, which is disallowed. The policy "
"has been ignored.";
DLOG(WARNING) << message;
}
void ContentSecurityPolicy::ReportValueForEmptyDirective(
const std::string& name, const std::string& value) {
std::string message =
"The Content Security Policy directive '" + name +
"' should be empty, but was delivered with a value of '" + value +
"'. The directive has been applied, and the value ignored.";
DLOG(WARNING) << message;
}
void ContentSecurityPolicy::ReportDirectiveAsSourceExpression(
const std::string& directive_name, const std::string& source_expression) {
std::string message = "The Content Security Policy directive '" +
directive_name + "' contains '" + source_expression +
"' as a source expression. Did you mean '" +
directive_name + " ...; " + source_expression +
"...' (note the semicolon)?";
DLOG(WARNING) << message;
}
void ContentSecurityPolicy::ReportInvalidSourceExpression(
const std::string& directive_name, const std::string& source) {
std::string message =
"The source list for Content Security Policy directive '" +
directive_name + "' contains an invalid source: '" + source +
"'. It will be ignored.";
if (LowerCaseEqualsASCII(source.c_str(), "'none'")) {
message = message +
" Note that 'none' has no effect unless it is the only "
"expression in the source list.";
}
DLOG(WARNING) << message;
}
void ContentSecurityPolicy::ReportInvalidPathCharacter(
const std::string& directive_name, const std::string& value,
char invalid_char) {
DCHECK(invalid_char == '#' || invalid_char == '?');
std::string message =
"The source list for Content Security Policy directive '";
message += directive_name;
message += "' contains a source with an invalid path: '";
message += value;
message += "'. ";
message +=
invalid_char == '?'
? "The query component, including the '?', will be ignored."
: "The fragment identifier, including the '#', will be ignored.";
DLOG(WARNING) << message;
}
void ContentSecurityPolicy::ReportDuplicateDirective(const std::string& name) {
std::string message =
"Ignoring duplicate Content-Security-Policy directive '" + name + "'.\n";
DLOG(WARNING) << message;
}
void ContentSecurityPolicy::ReportInvalidDirectiveValueCharacter(
const std::string& directive_name, const std::string& value) {
std::string message =
"The value for Content Security Policy directive '" + directive_name +
"' contains an invalid character: '" + value +
"'. Non-whitespace characters outside ASCII 0x21-0x7E must be "
"percent-encoded, as described in RFC 3986, section 2.1: "
"http://tools.ietf.org/html/rfc3986#section-2.1.";
DLOG(WARNING) << message;
}
void ContentSecurityPolicy::ReportInvalidReflectedXSS(
const std::string& invalid_value) {
std::string message =
"The 'reflected-xss' Content Security Policy directive has the invalid "
"value \"" +
invalid_value +
"\". Valid values are \"allow\", \"filter\", and \"block\".";
DLOG(WARNING) << message;
}
void ContentSecurityPolicy::ReportMissingReportURI(const std::string& policy) {
std::string message = "The Content Security Policy '";
message += policy;
message +=
"' was delivered in report-only mode, but does not specify a "
"'report-uri'; the policy will have no effect. Please either add a "
"'report-uri' directive, or deliver the policy via the "
"'Content-Security-Policy' header.";
DLOG(WARNING) << message;
}
void ContentSecurityPolicy::ReportReportOnlyInMeta(const std::string& header) {
std::string message = "The report-only Content Security Policy '" + header +
"' was delivered via a <meta> element, which is "
"disallowed. The policy has been ignored.";
DLOG(WARNING) << message;
}
void ContentSecurityPolicy::ReportInvalidSuboriginFlags(
const std::string& invalid_flags) {
std::string message =
"Error while parsing the 'suborigin' Content Security Policy "
"directive: " +
invalid_flags;
DLOG(WARNING) << message;
}
void ContentSecurityPolicy::ReportUnsupportedDirective(
const std::string& name) {
std::string lower_name = StringToLowerASCII(name);
std::string message;
if (lower_name == "allow") {
message =
"The 'allow' directive has been replaced with 'default-src'. Please "
"use that directive instead, as 'allow' has no effect.";
} else if (lower_name == "options") {
message =
"The 'options' directive has been replaced with 'unsafe-inline' and "
"'unsafe-eval' source expressions for the 'script-src' and 'style-src' "
"directives. Please use those directives instead, as 'options' has no "
"effect.";
} else if (lower_name == "policy-uri") {
message =
"The 'policy-uri' directive has been removed from the specification. "
"Please specify a complete policy via the Content-Security-Policy "
"header.";
} else if (IsDirectiveName(name)) {
message = "The Content-Security-Policy directive '" + name +
"' is implemented behind a flag which is currently disabled.\n";
} else {
message =
"Unrecognized Content-Security-Policy directive '" + name + "'.\n";
}
DLOG(WARNING) << message;
}
void ContentSecurityPolicy::ReportInvalidInReportOnly(const std::string& name) {
std::string message = "The Content Security Policy directive '" + name +
"' is ignored when delivered in a report-only policy.";
DLOG(WARNING) << message;
}
void ContentSecurityPolicy::ReportDirectiveNotSupportedInsideMeta(
const std::string& name) {
DLOG(WARNING) << "The " << name
<< " directive is not supported inside a <meta> element.";
}
bool ContentSecurityPolicy::AllowJavaScriptURLs(const std::string& context_url,
int context_line,
ReportingStatus status) const {
FOR_ALL_POLICIES_3(AllowJavaScriptURLs, context_url, context_line, status);
}
bool ContentSecurityPolicy::AllowInlineEventHandlers(
const std::string& context_url, int context_line,
ReportingStatus status) const {
FOR_ALL_POLICIES_3(AllowInlineEventHandlers, context_url, context_line,
status);
}
bool ContentSecurityPolicy::AllowInlineScript(const std::string& context_url,
int context_line,
const std::string& script_content,
ReportingStatus status) const {
FOR_ALL_POLICIES_4(AllowInlineScript, context_url, context_line, status,
script_content);
}
bool ContentSecurityPolicy::AllowInlineStyle(const std::string& context_url,
int context_line,
const std::string& style_content,
ReportingStatus status) const {
FOR_ALL_POLICIES_4(AllowInlineStyle, context_url, context_line, status,
style_content);
}
bool ContentSecurityPolicy::AllowEval(ReportingStatus status) const {
FOR_ALL_POLICIES_1(AllowEval, status);
}
bool ContentSecurityPolicy::AllowScriptFromSource(
const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
ContentSecurityPolicy::ReportingStatus reporting_status) const {
FOR_ALL_POLICIES_3(AllowScriptFromSource, url, redirect_status,
reporting_status);
}
bool ContentSecurityPolicy::AllowObjectFromSource(
const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
ContentSecurityPolicy::ReportingStatus reporting_status) const {
FOR_ALL_POLICIES_3(AllowObjectFromSource, url, redirect_status,
reporting_status);
}
bool ContentSecurityPolicy::AllowImageFromSource(
const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
ContentSecurityPolicy::ReportingStatus reporting_status) const {
FOR_ALL_POLICIES_3(AllowImageFromSource, url, redirect_status,
reporting_status);
}
bool ContentSecurityPolicy::AllowNavigateToSource(
const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
ContentSecurityPolicy::ReportingStatus reporting_status) const {
// Note that this is a Cobalt-specific policy to prevent navigation
// to any unexpected URLs. Navigation is restrictive by default, as
// opposed to the permissive policy for other directives.
if (!navigation_policy_) {
DLOG(ERROR) << "SetNavigationFallbackPolicy() was not called.";
return false;
}
// TODO: Re-enable respecting the navigation whitelist.
return navigation_policy_->AllowNavigateToSource(url, redirect_status,
reporting_status);
}
bool ContentSecurityPolicy::AllowStyleFromSource(
const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
ContentSecurityPolicy::ReportingStatus reporting_status) const {
FOR_ALL_POLICIES_3(AllowStyleFromSource, url, redirect_status,
reporting_status);
}
bool ContentSecurityPolicy::AllowFontFromSource(
const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
ContentSecurityPolicy::ReportingStatus reporting_status) const {
FOR_ALL_POLICIES_3(AllowFontFromSource, url, redirect_status,
reporting_status);
}
bool ContentSecurityPolicy::AllowMediaFromSource(
const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
ContentSecurityPolicy::ReportingStatus reporting_status) const {
FOR_ALL_POLICIES_3(AllowMediaFromSource, url, redirect_status,
reporting_status);
}
bool ContentSecurityPolicy::AllowConnectToSource(
const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
ContentSecurityPolicy::ReportingStatus reporting_status) const {
FOR_ALL_POLICIES_3(AllowConnectToSource, url, redirect_status,
reporting_status);
}
bool ContentSecurityPolicy::AllowFormAction(
const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
ContentSecurityPolicy::ReportingStatus reporting_status) const {
FOR_ALL_POLICIES_3(AllowFormAction, url, redirect_status, reporting_status);
}
bool ContentSecurityPolicy::AllowBaseURI(
const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
ContentSecurityPolicy::ReportingStatus reporting_status) const {
FOR_ALL_POLICIES_3(AllowBaseURI, url, redirect_status, reporting_status);
}
bool ContentSecurityPolicy::AllowManifestFromSource(
const GURL& url, ContentSecurityPolicy::RedirectStatus redirect_status,
ContentSecurityPolicy::ReportingStatus reporting_status) const {
FOR_ALL_POLICIES_3(AllowManifestFromSource, url, redirect_status,
reporting_status);
}
bool ContentSecurityPolicy::AllowScriptWithNonce(
const std::string& nonce) const {
FOR_ALL_POLICIES_1(AllowScriptNonce, nonce);
}
bool ContentSecurityPolicy::AllowStyleWithNonce(
const std::string& nonce) const {
FOR_ALL_POLICIES_1(AllowStyleNonce, nonce);
}
bool ContentSecurityPolicy::AllowScriptWithHash(
const std::string& source) const {
return CheckDigest<&DirectiveList::AllowScriptHash>(
source, script_hash_algorithms_used_, policies_);
}
bool ContentSecurityPolicy::AllowStyleWithHash(
const std::string& source) const {
return CheckDigest<&DirectiveList::AllowStyleHash>(
source, style_hash_algorithms_used_, policies_);
}
void ContentSecurityPolicy::NotifyUrlChanged(const GURL& url) {
url_ = url;
CreateSelfSource();
}
bool ContentSecurityPolicy::DidSetReferrerPolicy() const {
for (PolicyList::const_iterator it = policies_.begin(); it != policies_.end();
++it) {
if ((*it)->did_set_referrer_policy()) {
return true;
}
}
return false;
}
void ContentSecurityPolicy::CreateSelfSource() {
// Ensure that 'self' processes correctly.
self_scheme_ = url_.scheme();
SourceConfig config;
config.scheme = self_scheme_;
config.host = url_.host();
config.path.clear();
config.port = url_.IntPort();
config.host_wildcard = SourceConfig::kNoWildcard;
config.port_wildcard = SourceConfig::kNoWildcard;
self_source_.reset(new Source(this, config));
}
void ContentSecurityPolicy::AddPolicyFromHeaderValue(const std::string& header,
HeaderType type,
HeaderSource source) {
// If this is a report-only header inside a <meta> element, bail out.
if (source == kHeaderSourceMeta && type == kHeaderTypeReport) {
ReportReportOnlyInMeta(header);
return;
}
base::StringPiece characters(header);
const char* begin = characters.begin();
const char* end = characters.end();
// RFC2616, section 4.2 specifies that headers appearing multiple times can
// be combined with a comma. Walk the header string, and parse each comma
// separated chunk as a separate header.
const char* position = begin;
while (position < end) {
SkipUntil(&position, end, ',');
// header1,header2 OR header1
// ^ ^
base::StringPiece begin_piece(begin, static_cast<size_t>(position - begin));
scoped_ptr<DirectiveList> policy(
new DirectiveList(this, begin_piece, type, source));
if (type != kHeaderTypeReport && policy->did_set_referrer_policy()) {
// FIXME: We need a 'ReferrerPolicyUnset' enum to avoid confusing code
// like this.
referrer_policy_ = DidSetReferrerPolicy()
? MergeReferrerPolicies(referrer_policy_,
policy->referrer_policy())
: policy->referrer_policy();
}
if (!policy->AllowEval(kSuppressReport) &&
disable_eval_error_message_.empty()) {
disable_eval_error_message_ = policy->eval_disabled_error_message();
}
policies_.push_back(policy.release());
// Skip the comma, and begin the next header from the current position.
DCHECK(position == end || *position == ',');
SkipExactly(&position, end, ',');
begin = position;
}
}
} // namespace csp
} // namespace cobalt