blob: b05faa096f10e9aceb5df73f0bbadc07aa932d7c [file] [log] [blame]
/*
* Copyright (C) 2011 Google, 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 COMPUTER, 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 "third_party/blink/renderer/core/frame/csp/content_security_policy.h"
#include <memory>
#include <utility>
#include "base/debug/dump_without_crashing.h"
#include "services/network/public/cpp/web_sandbox_flags.h"
#include "services/network/public/mojom/web_sandbox_flags.mojom-blink.h"
#include "third_party/blink/public/mojom/fetch/fetch_api_request.mojom-blink.h"
#include "third_party/blink/public/mojom/security_context/insecure_request_policy.mojom-blink.h"
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/public/platform/task_type.h"
#include "third_party/blink/public/platform/web_url_request.h"
#include "third_party/blink/renderer/bindings/core/v8/isolated_world_csp.h"
#include "third_party/blink/renderer/bindings/core/v8/script_controller.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_security_policy_violation_event_init.h"
#include "third_party/blink/renderer/core/dom/dom_node_ids.h"
#include "third_party/blink/renderer/core/dom/dom_string_list.h"
#include "third_party/blink/renderer/core/dom/element.h"
#include "third_party/blink/renderer/core/dom/events/event_queue.h"
#include "third_party/blink/renderer/core/frame/csp/csp_directive_list.h"
#include "third_party/blink/renderer/core/frame/csp/csp_source.h"
#include "third_party/blink/renderer/core/frame/frame_client.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/local_frame_client.h"
#include "third_party/blink/renderer/core/frame/location.h"
#include "third_party/blink/renderer/core/html/html_script_element.h"
#include "third_party/blink/renderer/core/inspector/console_message.h"
#include "third_party/blink/renderer/platform/bindings/dom_wrapper_world.h"
#include "third_party/blink/renderer/platform/heap/heap.h"
#include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
#include "third_party/blink/renderer/platform/json/json_values.h"
#include "third_party/blink/renderer/platform/loader/fetch/integrity_metadata.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_request.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_response.h"
#include "third_party/blink/renderer/platform/network/content_security_policy_parsers.h"
#include "third_party/blink/renderer/platform/network/content_security_policy_response_headers.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/weborigin/known_ports.h"
#include "third_party/blink/renderer/platform/weborigin/kurl.h"
#include "third_party/blink/renderer/platform/weborigin/security_origin.h"
#include "third_party/blink/renderer/platform/wtf/text/parsing_utilities.h"
#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
#include "third_party/blink/renderer/platform/wtf/text/string_hasher.h"
#include "third_party/blink/renderer/platform/wtf/text/string_utf8_adaptor.h"
#include "third_party/blink/renderer/platform/wtf/wtf_size_t.h"
#include "v8/include/v8.h"
namespace blink {
using network::mojom::ContentSecurityPolicySource;
using network::mojom::ContentSecurityPolicyType;
namespace {
// Helper function that returns true if the given |header_type| should be
// checked when the CheckHeaderType is |check_header_type|.
bool CheckHeaderTypeMatches(
ContentSecurityPolicy::CheckHeaderType check_header_type,
ContentSecurityPolicyType header_type) {
switch (check_header_type) {
case ContentSecurityPolicy::CheckHeaderType::kCheckAll:
return true;
case ContentSecurityPolicy::CheckHeaderType::kCheckReportOnly:
return header_type == ContentSecurityPolicyType::kReport;
case ContentSecurityPolicy::CheckHeaderType::kCheckEnforce:
return header_type == ContentSecurityPolicyType::kEnforce;
}
NOTREACHED();
return false;
}
int32_t HashAlgorithmsUsed(
const network::mojom::blink::CSPSourceList* source_list) {
int32_t hash_algorithms_used = 0;
if (!source_list)
return hash_algorithms_used;
for (const auto& hash : source_list->hashes) {
hash_algorithms_used |= static_cast<int32_t>(hash->algorithm);
}
return hash_algorithms_used;
}
} // namespace
bool ContentSecurityPolicy::IsNonceableElement(const Element* element) {
if (element->nonce().IsNull())
return false;
bool nonceable = true;
// To prevent an attacker from hijacking an existing nonce via a dangling
// markup injection, we walk through the attributes of each nonced script
// element: if their names or values contain "<script" or "<style", we won't
// apply the nonce when loading script.
//
// See http://blog.innerht.ml/csp-2015/#danglingmarkupinjection for an example
// of the kind of attack this is aimed at mitigating.
if (element->HasDuplicateAttribute())
nonceable = false;
if (nonceable) {
static const char kScriptString[] = "<SCRIPT";
static const char kStyleString[] = "<STYLE";
for (const Attribute& attr : element->Attributes()) {
const AtomicString& name = attr.LocalName();
const AtomicString& value = attr.Value();
if (name.FindIgnoringASCIICase(kScriptString) != WTF::kNotFound ||
name.FindIgnoringASCIICase(kStyleString) != WTF::kNotFound ||
value.FindIgnoringASCIICase(kScriptString) != WTF::kNotFound ||
value.FindIgnoringASCIICase(kStyleString) != WTF::kNotFound) {
nonceable = false;
break;
}
}
}
UseCounter::Count(
element->GetExecutionContext(),
nonceable ? WebFeature::kCleanScriptElementWithNonce
: WebFeature::kPotentiallyInjectedScriptElementWithNonce);
return nonceable;
}
static WebFeature GetUseCounterHelperType(ContentSecurityPolicyType type) {
switch (type) {
case ContentSecurityPolicyType::kEnforce:
return WebFeature::kContentSecurityPolicy;
case ContentSecurityPolicyType::kReport:
return WebFeature::kContentSecurityPolicyReportOnly;
}
NOTREACHED();
return WebFeature::kNumberOfFeatures;
}
ContentSecurityPolicy::ContentSecurityPolicy()
: delegate_(nullptr),
override_inline_style_allowed_(false),
script_hash_algorithms_used_(kContentSecurityPolicyHashAlgorithmNone),
style_hash_algorithms_used_(kContentSecurityPolicyHashAlgorithmNone),
sandbox_mask_(network::mojom::blink::WebSandboxFlags::kNone),
require_trusted_types_(false),
insecure_request_policy_(
mojom::blink::InsecureRequestPolicy::kLeaveInsecureRequestsAlone) {}
bool ContentSecurityPolicy::IsBound() {
return delegate_;
}
void ContentSecurityPolicy::BindToDelegate(
ContentSecurityPolicyDelegate& delegate) {
// TODO(crbug.com/915954): Add DCHECK(!delegate_). It seems some call sites
// call this function multiple times.
delegate_ = &delegate;
ApplyPolicySideEffectsToDelegate();
// Report use counters for all the policies that have been parsed until now.
ReportUseCounters(policies_);
}
void ContentSecurityPolicy::ApplyPolicySideEffectsToDelegate() {
DCHECK(delegate_);
// Set mixed content checking and sandbox flags, then dump all the parsing
// error messages, then poke at histograms.
if (sandbox_mask_ != network::mojom::blink::WebSandboxFlags::kNone) {
Count(WebFeature::kSandboxViaCSP);
delegate_->SetSandboxFlags(sandbox_mask_);
}
if (require_trusted_types_) {
delegate_->SetRequireTrustedTypes();
Count(WebFeature::kTrustedTypesEnabled);
}
delegate_->AddInsecureRequestPolicy(insecure_request_policy_);
for (const auto& console_message : console_messages_)
delegate_->AddConsoleMessage(console_message);
console_messages_.clear();
// We disable 'eval()' even in the case of report-only policies, and rely on
// the check in the V8Initializer::codeGenerationCheckCallbackInMainThread
// callback to determine whether the call should execute or not.
if (!disable_eval_error_message_.IsNull())
delegate_->DisableEval(disable_eval_error_message_);
}
void ContentSecurityPolicy::ReportUseCounters(
const Vector<network::mojom::blink::ContentSecurityPolicyPtr>& policies) {
for (const auto& policy : policies) {
Count(GetUseCounterHelperType(policy->header->type));
if (CSPDirectiveListAllowDynamic(*policy,
CSPDirectiveName::ScriptSrcAttr) ||
CSPDirectiveListAllowDynamic(*policy,
CSPDirectiveName::ScriptSrcElem)) {
Count(WebFeature::kCSPWithStrictDynamic);
}
if (CSPDirectiveListAllowEval(*policy, this,
ReportingDisposition::kSuppressReporting,
kWillNotThrowException, g_empty_string)) {
Count(WebFeature::kCSPWithUnsafeEval);
}
// We consider a policy to be "reasonably secure" if it:
//
// 1. Asserts `object-src 'none'`.
// 2. Asserts `base-uri 'none'` or `base-uri 'self'`.
// 3. Avoids URL-based matching, in favor of hashes and nonces.
//
// https://chromium.googlesource.com/chromium/src/+/master/docs/security/web-mitigation-metrics.md
// has more detail.
if (CSPDirectiveListIsObjectRestrictionReasonable(*policy)) {
Count(policy->header->type == ContentSecurityPolicyType::kEnforce
? WebFeature::kCSPWithReasonableObjectRestrictions
: WebFeature::kCSPROWithReasonableObjectRestrictions);
}
if (CSPDirectiveListIsBaseRestrictionReasonable(*policy)) {
Count(policy->header->type == ContentSecurityPolicyType::kEnforce
? WebFeature::kCSPWithReasonableBaseRestrictions
: WebFeature::kCSPROWithReasonableBaseRestrictions);
}
if (CSPDirectiveListIsScriptRestrictionReasonable(*policy)) {
Count(policy->header->type == ContentSecurityPolicyType::kEnforce
? WebFeature::kCSPWithReasonableScriptRestrictions
: WebFeature::kCSPROWithReasonableScriptRestrictions);
}
if (CSPDirectiveListIsObjectRestrictionReasonable(*policy) &&
CSPDirectiveListIsBaseRestrictionReasonable(*policy) &&
CSPDirectiveListIsScriptRestrictionReasonable(*policy)) {
Count(policy->header->type == ContentSecurityPolicyType::kEnforce
? WebFeature::kCSPWithReasonableRestrictions
: WebFeature::kCSPROWithReasonableRestrictions);
if (!CSPDirectiveListAllowDynamic(*policy,
CSPDirectiveName::ScriptSrcElem)) {
Count(policy->header->type == ContentSecurityPolicyType::kEnforce
? WebFeature::kCSPWithBetterThanReasonableRestrictions
: WebFeature::kCSPROWithBetterThanReasonableRestrictions);
}
}
if (CSPDirectiveListRequiresTrustedTypes(*policy)) {
Count(CSPDirectiveListIsReportOnly(*policy)
? WebFeature::kTrustedTypesEnabledReportOnly
: WebFeature::kTrustedTypesEnabledEnforcing);
}
if (policy->trusted_types && policy->trusted_types->allow_duplicates) {
Count(WebFeature::kTrustedTypesAllowDuplicates);
}
}
}
ContentSecurityPolicy::~ContentSecurityPolicy() = default;
void ContentSecurityPolicy::Trace(Visitor* visitor) const {
visitor->Trace(delegate_);
visitor->Trace(console_messages_);
}
void ContentSecurityPolicy::CopyStateFrom(const ContentSecurityPolicy* other) {
DCHECK(policies_.IsEmpty());
policies_ = mojo::Clone(other->policies_);
for (const auto& policy : policies_)
ComputeInternalStateForParsedPolicy(*policy);
if (delegate_)
ReportAccumulatedHeaders();
}
void ContentSecurityPolicy::DidReceiveHeaders(
const ContentSecurityPolicyResponseHeaders& headers) {
scoped_refptr<SecurityOrigin> self_origin =
SecurityOrigin::Create(headers.ResponseUrl());
if (headers.ShouldParseWasmEval())
supports_wasm_eval_ = true;
Vector<network::mojom::blink::ContentSecurityPolicyPtr> parsed_policies;
if (!headers.ContentSecurityPolicy().IsEmpty()) {
parsed_policies = Parse(headers.ContentSecurityPolicy(), *self_origin,
ContentSecurityPolicyType::kEnforce,
ContentSecurityPolicySource::kHTTP);
}
if (!headers.ContentSecurityPolicyReportOnly().IsEmpty()) {
for (auto& policy : Parse(headers.ContentSecurityPolicyReportOnly(),
*self_origin, ContentSecurityPolicyType::kReport,
ContentSecurityPolicySource::kHTTP)) {
parsed_policies.push_back(std::move(policy));
}
}
AddPolicies(std::move(parsed_policies));
}
// static
WTF::Vector<network::mojom::blink::ContentSecurityPolicyPtr>
ContentSecurityPolicy::ParseHeaders(
const ContentSecurityPolicyResponseHeaders& headers) {
auto* content_security_policy = MakeGarbageCollected<ContentSecurityPolicy>();
content_security_policy->DidReceiveHeaders(headers);
return std::move(content_security_policy->policies_);
}
void ContentSecurityPolicy::DidReceiveHeader(
const String& header,
const SecurityOrigin& self_origin,
ContentSecurityPolicyType type,
ContentSecurityPolicySource source) {
AddPolicies(Parse(header, self_origin, type, source));
}
void ContentSecurityPolicy::AddPolicies(
Vector<network::mojom::blink::ContentSecurityPolicyPtr> policies) {
Vector<network::mojom::blink::ContentSecurityPolicyPtr> policies_to_report;
if (delegate_) {
policies_to_report = mojo::Clone(policies);
}
for (network::mojom::blink::ContentSecurityPolicyPtr& policy : policies) {
ComputeInternalStateForParsedPolicy(*policy);
policies_.push_back(std::move(policy));
}
// If this ContentSecurityPolicy is not bound to a delegate yet, return. The
// following logic will be executed in BindToDelegate when that will happen.
if (!delegate_)
return;
ApplyPolicySideEffectsToDelegate();
ReportUseCounters(policies_to_report);
// Notify about the new header, so that it can be reported back to the
// browser process. This is needed in order to:
// 1) replicate CSP directives (i.e. frame-src) to OOPIFs (only for now /
// short-term).
// 2) enforce CSP in the browser process (long-term - see
// https://crbug.com/376522).
// TODO(arthursonzogni): policies are actually replicated (1) and some of
// them are enforced on the browser process (2). Stop doing (1) when (2) is
// finished.
delegate_->DidAddContentSecurityPolicies(std::move(policies_to_report));
}
Vector<network::mojom::blink::ContentSecurityPolicyPtr>
ContentSecurityPolicy::Parse(const String& header,
const SecurityOrigin& self_origin,
ContentSecurityPolicyType type,
ContentSecurityPolicySource source) {
Vector<network::mojom::blink::ContentSecurityPolicyPtr> policies;
// If this is a report-only header inside a <meta> element, bail out.
if (source == ContentSecurityPolicySource::kMeta &&
type == ContentSecurityPolicyType::kReport) {
ReportReportOnlyInMeta(header);
return policies;
}
Vector<UChar> characters;
header.AppendTo(characters);
const UChar* begin = characters.data();
const UChar* end = begin + characters.size();
// 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 UChar* position = begin;
while (position < end) {
SkipUntil<UChar>(position, end, ',');
// header1,header2 OR header1
// ^ ^
policies.push_back(CSPDirectiveListParse(this, begin, position, self_origin,
type, source));
// Skip the comma, and begin the next header from the current position.
DCHECK(position == end || *position == ',');
SkipExactly<UChar>(position, end, ',');
begin = position;
}
return policies;
}
void ContentSecurityPolicy::ComputeInternalStateForParsedPolicy(
const network::mojom::blink::ContentSecurityPolicy& csp) {
if (csp.header->source == ContentSecurityPolicySource::kHTTP)
header_delivered_ = true;
if (csp.block_all_mixed_content && !CSPDirectiveListIsReportOnly(csp))
EnforceStrictMixedContentChecking();
if (CSPDirectiveListRequiresTrustedTypes(csp))
RequireTrustedTypes();
EnforceSandboxFlags(csp.sandbox);
if (csp.upgrade_insecure_requests)
UpgradeInsecureRequests();
String disable_eval_message;
if (CSPDirectiveListShouldDisableEval(csp, disable_eval_message) &&
disable_eval_error_message_.IsNull()) {
disable_eval_error_message_ = disable_eval_message;
}
for (const auto& directive : csp.directives) {
switch (directive.key) {
case CSPDirectiveName::DefaultSrc:
// TODO(mkwst) It seems unlikely that developers would use different
// algorithms for scripts and styles. We may want to combine the
// usesScriptHashAlgorithms() and usesStyleHashAlgorithms.
UsesScriptHashAlgorithms(HashAlgorithmsUsed(directive.value.get()));
UsesStyleHashAlgorithms(HashAlgorithmsUsed(directive.value.get()));
break;
case CSPDirectiveName::ScriptSrc:
case CSPDirectiveName::ScriptSrcAttr:
case CSPDirectiveName::ScriptSrcElem:
UsesScriptHashAlgorithms(HashAlgorithmsUsed(directive.value.get()));
break;
case CSPDirectiveName::StyleSrc:
case CSPDirectiveName::StyleSrcAttr:
case CSPDirectiveName::StyleSrcElem:
UsesStyleHashAlgorithms(HashAlgorithmsUsed(directive.value.get()));
break;
default:
break;
}
}
}
void ContentSecurityPolicy::ReportAccumulatedHeaders() const {
DCHECK(delegate_);
delegate_->DidAddContentSecurityPolicies(mojo::Clone(GetParsedPolicies()));
}
void ContentSecurityPolicy::SetOverrideAllowInlineStyle(bool value) {
override_inline_style_allowed_ = value;
}
// static
void ContentSecurityPolicy::FillInCSPHashValues(
const String& source,
uint8_t hash_algorithms_used,
Vector<network::mojom::blink::CSPHashSourcePtr>& csp_hash_values) {
// Any additions or subtractions from this struct should also modify the
// respective entries in the kSupportedPrefixes array in
// SourceListDirective::parseHash().
static const struct {
network::mojom::blink::CSPHashAlgorithm csp_hash_algorithm;
HashAlgorithm algorithm;
} kAlgorithmMap[] = {
{network::mojom::blink::CSPHashAlgorithm::SHA256, kHashAlgorithmSha256},
{network::mojom::blink::CSPHashAlgorithm::SHA384, kHashAlgorithmSha384},
{network::mojom::blink::CSPHashAlgorithm::SHA512, kHashAlgorithmSha512}};
// Only bother normalizing the source/computing digests if there are any
// checks to be done.
if (hash_algorithms_used == kContentSecurityPolicyHashAlgorithmNone)
return;
StringUTF8Adaptor utf8_source(
source, kStrictUTF8ConversionReplacingUnpairedSurrogatesWithFFFD);
for (const auto& algorithm_map : kAlgorithmMap) {
DigestValue digest;
if (static_cast<int32_t>(algorithm_map.csp_hash_algorithm) &
hash_algorithms_used) {
bool digest_success =
ComputeDigest(algorithm_map.algorithm, utf8_source.data(),
utf8_source.size(), digest);
if (digest_success) {
csp_hash_values.push_back(network::mojom::blink::CSPHashSource::New(
algorithm_map.csp_hash_algorithm, Vector<uint8_t>(digest)));
}
}
}
}
// static
bool ContentSecurityPolicy::CheckHashAgainstPolicy(
Vector<network::mojom::blink::CSPHashSourcePtr>& csp_hash_values,
const network::mojom::blink::ContentSecurityPolicy& csp,
InlineType inline_type) {
for (const auto& csp_hash_value : csp_hash_values) {
if (CSPDirectiveListAllowHash(csp, *csp_hash_value, inline_type))
return true;
}
return false;
}
// https://w3c.github.io/webappsec-csp/#should-block-inline
bool ContentSecurityPolicy::AllowInline(
InlineType inline_type,
Element* element,
const String& content,
const String& nonce,
const String& context_url,
const WTF::OrdinalNumber& context_line,
ReportingDisposition reporting_disposition) {
DCHECK(element || inline_type == InlineType::kScriptAttribute ||
inline_type == InlineType::kNavigation);
const bool is_script = IsScriptInlineType(inline_type);
if (!is_script && override_inline_style_allowed_) {
return true;
}
Vector<network::mojom::blink::CSPHashSourcePtr> csp_hash_values;
FillInCSPHashValues(
content,
is_script ? script_hash_algorithms_used_ : style_hash_algorithms_used_,
csp_hash_values);
// Step 2. Let result be "Allowed". [spec text]
bool is_allowed = true;
// Step 3. For each policy in element’s Document's global object’s CSP list:
// [spec text]
for (const auto& policy : policies_) {
// May be allowed by hash, if 'unsafe-hashes' is present in a policy.
// Check against the digest of the |content| and also check whether inline
// script is allowed.
is_allowed &=
CheckHashAgainstPolicy(csp_hash_values, *policy, inline_type) ||
CSPDirectiveListAllowInline(*policy, this, inline_type, element,
content, nonce, context_url, context_line,
reporting_disposition);
}
return is_allowed;
}
bool ContentSecurityPolicy::IsScriptInlineType(InlineType inline_type) {
switch (inline_type) {
case ContentSecurityPolicy::InlineType::kNavigation:
case ContentSecurityPolicy::InlineType::kScriptAttribute:
case ContentSecurityPolicy::InlineType::kScript:
return true;
case ContentSecurityPolicy::InlineType::kStyleAttribute:
case ContentSecurityPolicy::InlineType::kStyle:
return false;
}
}
bool ContentSecurityPolicy::ShouldCheckEval() const {
for (const auto& policy : policies_) {
if (CSPDirectiveListShouldCheckEval(*policy))
return true;
}
return IsRequireTrustedTypes();
}
bool ContentSecurityPolicy::AllowEval(
ReportingDisposition reporting_disposition,
ContentSecurityPolicy::ExceptionStatus exception_status,
const String& script_content) {
bool is_allowed = true;
for (const auto& policy : policies_) {
is_allowed &= CSPDirectiveListAllowEval(
*policy, this, reporting_disposition, exception_status, script_content);
}
return is_allowed;
}
bool ContentSecurityPolicy::AllowWasmEval(
ReportingDisposition reporting_disposition,
ContentSecurityPolicy::ExceptionStatus exception_status,
const String& script_content) {
bool is_allowed = true;
for (const auto& policy : policies_) {
is_allowed &= CSPDirectiveListAllowWasmEval(
*policy, this, reporting_disposition, exception_status, script_content);
}
return is_allowed;
}
String ContentSecurityPolicy::EvalDisabledErrorMessage() const {
for (const auto& policy : policies_) {
String message;
if (CSPDirectiveListShouldDisableEval(*policy, message))
return message;
}
return String();
}
static base::Optional<CSPDirectiveName> GetDirectiveTypeFromRequestContextType(
mojom::blink::RequestContextType context) {
switch (context) {
case mojom::blink::RequestContextType::AUDIO:
case mojom::blink::RequestContextType::TRACK:
case mojom::blink::RequestContextType::VIDEO:
return CSPDirectiveName::MediaSrc;
case mojom::blink::RequestContextType::BEACON:
case mojom::blink::RequestContextType::EVENT_SOURCE:
case mojom::blink::RequestContextType::FETCH:
case mojom::blink::RequestContextType::PING:
case mojom::blink::RequestContextType::XML_HTTP_REQUEST:
case mojom::blink::RequestContextType::SUBRESOURCE:
case mojom::blink::RequestContextType::SUBRESOURCE_WEBBUNDLE:
return CSPDirectiveName::ConnectSrc;
case mojom::blink::RequestContextType::EMBED:
case mojom::blink::RequestContextType::OBJECT:
return CSPDirectiveName::ObjectSrc;
case mojom::blink::RequestContextType::PREFETCH:
return CSPDirectiveName::PrefetchSrc;
case mojom::blink::RequestContextType::FAVICON:
case mojom::blink::RequestContextType::IMAGE:
case mojom::blink::RequestContextType::IMAGE_SET:
return CSPDirectiveName::ImgSrc;
case mojom::blink::RequestContextType::FONT:
return CSPDirectiveName::FontSrc;
case mojom::blink::RequestContextType::FORM:
return CSPDirectiveName::FormAction;
case mojom::blink::RequestContextType::FRAME:
case mojom::blink::RequestContextType::IFRAME:
return CSPDirectiveName::FrameSrc;
case mojom::blink::RequestContextType::IMPORT:
case mojom::blink::RequestContextType::SCRIPT:
case mojom::blink::RequestContextType::XSLT:
return CSPDirectiveName::ScriptSrcElem;
case mojom::blink::RequestContextType::MANIFEST:
return CSPDirectiveName::ManifestSrc;
case mojom::blink::RequestContextType::SERVICE_WORKER:
case mojom::blink::RequestContextType::SHARED_WORKER:
case mojom::blink::RequestContextType::WORKER:
return CSPDirectiveName::WorkerSrc;
case mojom::blink::RequestContextType::STYLE:
return CSPDirectiveName::StyleSrcElem;
case mojom::blink::RequestContextType::CSP_REPORT:
case mojom::blink::RequestContextType::DOWNLOAD:
case mojom::blink::RequestContextType::HYPERLINK:
case mojom::blink::RequestContextType::INTERNAL:
case mojom::blink::RequestContextType::LOCATION:
case mojom::blink::RequestContextType::PLUGIN:
case mojom::blink::RequestContextType::UNSPECIFIED:
return base::nullopt;
}
}
bool ContentSecurityPolicy::AllowRequest(
mojom::blink::RequestContextType context,
network::mojom::RequestDestination request_destination,
const KURL& url,
const String& nonce,
const IntegrityMetadataSet& integrity_metadata,
ParserDisposition parser_disposition,
const KURL& url_before_redirects,
RedirectStatus redirect_status,
ReportingDisposition reporting_disposition,
CheckHeaderType check_header_type) {
base::Optional<CSPDirectiveName> type =
GetDirectiveTypeFromRequestContextType(context);
if (!type)
return true;
return AllowFromSource(*type, url, url_before_redirects, redirect_status,
reporting_disposition, check_header_type, nonce,
integrity_metadata, parser_disposition);
}
void ContentSecurityPolicy::UsesScriptHashAlgorithms(uint8_t algorithms) {
script_hash_algorithms_used_ |= algorithms;
}
void ContentSecurityPolicy::UsesStyleHashAlgorithms(uint8_t algorithms) {
style_hash_algorithms_used_ |= algorithms;
}
bool ContentSecurityPolicy::AllowFromSource(
CSPDirectiveName type,
const KURL& url,
const KURL& url_before_redirects,
RedirectStatus redirect_status,
ReportingDisposition reporting_disposition,
CheckHeaderType check_header_type,
const String& nonce,
const IntegrityMetadataSet& hashes,
ParserDisposition parser_disposition) {
SchemeRegistry::PolicyAreas area = SchemeRegistry::kPolicyAreaAll;
if (type == CSPDirectiveName::ImgSrc)
area = SchemeRegistry::kPolicyAreaImage;
else if (type == CSPDirectiveName::StyleSrcElem)
area = SchemeRegistry::kPolicyAreaStyle;
if (ShouldBypassContentSecurityPolicy(url, area)) {
if (type != CSPDirectiveName::ScriptSrcElem)
return true;
Count(parser_disposition == kParserInserted
? WebFeature::kScriptWithCSPBypassingSchemeParserInserted
: WebFeature::kScriptWithCSPBypassingSchemeNotParserInserted);
// If we're running experimental features, bypass CSP only for
// non-parser-inserted resources whose scheme otherwise bypasses CSP. If
// we're not running experimental features, bypass CSP for all resources
// regardless of parser state. Once we have more data via the
// 'ScriptWithCSPBypassingScheme*' metrics, make a decision about what
// behavior to ship. https://crbug.com/653521
if ((parser_disposition == kNotParserInserted ||
!ExperimentalFeaturesEnabled()) &&
// The schemes where javascript:-URLs are blocked are usually
// privileged pages, so do not allow the CSP to be bypassed either.
!SchemeRegistry::ShouldTreatURLSchemeAsNotAllowingJavascriptURLs(
delegate_->GetSecurityOrigin()->Protocol())) {
return true;
}
}
bool is_allowed = true;
for (const auto& policy : policies_) {
if (!CheckHeaderTypeMatches(check_header_type, policy->header->type))
continue;
is_allowed &= CSPDirectiveListAllowFromSource(
*policy, this, type, url, url_before_redirects, redirect_status,
reporting_disposition, nonce, hashes, parser_disposition);
}
return is_allowed;
}
bool ContentSecurityPolicy::AllowBaseURI(const KURL& url) {
// `base-uri` isn't affected by 'upgrade-insecure-requests', so we use
// CheckHeaderType::kCheckAll to check both report-only and enforce headers
// here.
return AllowFromSource(CSPDirectiveName::BaseURI, url, url,
RedirectStatus::kNoRedirect);
}
bool ContentSecurityPolicy::AllowConnectToSource(
const KURL& url,
const KURL& url_before_redirects,
RedirectStatus redirect_status,
ReportingDisposition reporting_disposition,
CheckHeaderType check_header_type) {
return AllowFromSource(CSPDirectiveName::ConnectSrc, url,
url_before_redirects, redirect_status,
reporting_disposition, check_header_type);
}
bool ContentSecurityPolicy::AllowFormAction(const KURL& url) {
return AllowFromSource(CSPDirectiveName::FormAction, url, url,
RedirectStatus::kNoRedirect);
}
bool ContentSecurityPolicy::AllowImageFromSource(
const KURL& url,
const KURL& url_before_redirects,
RedirectStatus redirect_status,
ReportingDisposition reporting_disposition,
CheckHeaderType check_header_type) {
return AllowFromSource(CSPDirectiveName::ImgSrc, url, url_before_redirects,
redirect_status, reporting_disposition,
check_header_type);
}
bool ContentSecurityPolicy::AllowMediaFromSource(const KURL& url) {
return AllowFromSource(CSPDirectiveName::MediaSrc, url, url,
RedirectStatus::kNoRedirect);
}
bool ContentSecurityPolicy::AllowObjectFromSource(const KURL& url) {
return AllowFromSource(CSPDirectiveName::ObjectSrc, url, url,
RedirectStatus::kNoRedirect);
}
bool ContentSecurityPolicy::AllowScriptFromSource(
const KURL& url,
const String& nonce,
const IntegrityMetadataSet& hashes,
ParserDisposition parser_disposition,
const KURL& url_before_redirects,
RedirectStatus redirect_status,
ReportingDisposition reporting_disposition,
CheckHeaderType check_header_type) {
return AllowFromSource(CSPDirectiveName::ScriptSrcElem, url,
url_before_redirects, redirect_status,
reporting_disposition, check_header_type, nonce,
hashes, parser_disposition);
}
bool ContentSecurityPolicy::AllowWorkerContextFromSource(const KURL& url) {
return AllowFromSource(CSPDirectiveName::WorkerSrc, url, url,
RedirectStatus::kNoRedirect);
}
// The return value indicates whether the policy is allowed or not.
// If the return value is false, the out-parameter violation_details indicates
// the type of the violation, and if the return value is true,
// it indicates if a report-only violation occurred.
bool ContentSecurityPolicy::AllowTrustedTypePolicy(
const String& policy_name,
bool is_duplicate,
AllowTrustedTypePolicyDetails& violation_details) {
bool is_allowed = true;
violation_details = AllowTrustedTypePolicyDetails::kAllowed;
for (const auto& policy : policies_) {
if (!CheckHeaderTypeMatches(CheckHeaderType::kCheckAll,
policy->header->type)) {
continue;
}
auto new_violation_details = AllowTrustedTypePolicyDetails::kAllowed;
bool new_allowed = CSPDirectiveListAllowTrustedTypePolicy(
*policy, this, policy_name, is_duplicate, new_violation_details);
// Report the first violation that is enforced.
// If there is none, report the first violation that is report-only.
if ((is_allowed && !new_allowed) ||
violation_details == AllowTrustedTypePolicyDetails::kAllowed) {
violation_details = new_violation_details;
}
is_allowed &= new_allowed;
}
return is_allowed;
}
bool ContentSecurityPolicy::AllowTrustedTypeAssignmentFailure(
const String& message,
const String& sample,
const String& sample_prefix) {
bool allow = true;
for (const auto& policy : policies_) {
allow &= CSPDirectiveListAllowTrustedTypeAssignmentFailure(
*policy, this, message, sample, sample_prefix);
}
return allow;
}
bool ContentSecurityPolicy::IsActive() const {
return !policies_.IsEmpty();
}
bool ContentSecurityPolicy::IsActiveForConnections() const {
for (const auto& policy : policies_) {
if (CSPDirectiveListIsActiveForConnections(*policy))
return true;
}
return false;
}
const KURL ContentSecurityPolicy::FallbackUrlForPlugin() const {
return delegate_ ? delegate_->Url() : KURL();
}
void ContentSecurityPolicy::EnforceSandboxFlags(
network::mojom::blink::WebSandboxFlags mask) {
sandbox_mask_ |= mask;
}
void ContentSecurityPolicy::RequireTrustedTypes() {
// We store whether CSP demands a policy. The caller still needs to check
// whether the feature is enabled in the first place.
require_trusted_types_ = true;
}
void ContentSecurityPolicy::EnforceStrictMixedContentChecking() {
insecure_request_policy_ |=
mojom::blink::InsecureRequestPolicy::kBlockAllMixedContent;
}
void ContentSecurityPolicy::UpgradeInsecureRequests() {
insecure_request_policy_ |=
mojom::blink::InsecureRequestPolicy::kUpgradeInsecureRequests;
}
static String StripURLForUseInReport(const SecurityOrigin* security_origin,
const KURL& url,
RedirectStatus redirect_status,
CSPDirectiveName effective_type) {
if (!url.IsValid())
return String();
if (!url.IsHierarchical() || url.ProtocolIs("file"))
return url.Protocol();
// Until we're more careful about the way we deal with navigations in frames
// (and, by extension, in plugin documents), strip cross-origin 'frame-src'
// and 'object-src' violations down to an origin. https://crbug.com/633306
bool can_safely_expose_url =
security_origin->CanRequest(url) ||
(redirect_status == RedirectStatus::kNoRedirect &&
effective_type != CSPDirectiveName::FrameSrc &&
effective_type != CSPDirectiveName::ObjectSrc);
if (can_safely_expose_url) {
// 'KURL::strippedForUseAsReferrer()' dumps 'String()' for non-webby URLs.
// It's better for developers if we return the origin of those URLs rather
// than nothing.
if (url.ProtocolIsInHTTPFamily())
return url.StrippedForUseAsReferrer();
}
return SecurityOrigin::Create(url)->ToString();
}
namespace {
std::unique_ptr<SourceLocation> GatherSecurityPolicyViolationEventData(
SecurityPolicyViolationEventInit* init,
ContentSecurityPolicyDelegate* delegate,
const String& directive_text,
CSPDirectiveName effective_type,
const KURL& blocked_url,
const String& header,
RedirectStatus redirect_status,
ContentSecurityPolicyType header_type,
ContentSecurityPolicy::ContentSecurityPolicyViolationType violation_type,
std::unique_ptr<SourceLocation> source_location,
const String& script_source,
const String& sample_prefix) {
if (effective_type == CSPDirectiveName::FrameAncestors) {
// If this load was blocked via 'frame-ancestors', then the URL of
// |document| has not yet been initialized. In this case, we'll set both
// 'documentURI' and 'blockedURI' to the blocked document's URL.
String stripped_url = StripURLForUseInReport(
delegate->GetSecurityOrigin(), blocked_url, RedirectStatus::kNoRedirect,
CSPDirectiveName::DefaultSrc);
init->setDocumentURI(stripped_url);
init->setBlockedURI(stripped_url);
} else {
String stripped_url = StripURLForUseInReport(
delegate->GetSecurityOrigin(), delegate->Url(),
RedirectStatus::kNoRedirect, CSPDirectiveName::DefaultSrc);
init->setDocumentURI(stripped_url);
switch (violation_type) {
case ContentSecurityPolicy::kInlineViolation:
init->setBlockedURI("inline");
break;
case ContentSecurityPolicy::kEvalViolation:
init->setBlockedURI("eval");
break;
case ContentSecurityPolicy::kURLViolation:
// We pass RedirectStatus::kNoRedirect so that StripURLForUseInReport
// does not strip path and query from the URL. This is safe since
// blocked_url at this point is always the original url (before
// redirects).
init->setBlockedURI(StripURLForUseInReport(
delegate->GetSecurityOrigin(), blocked_url,
RedirectStatus::kNoRedirect, effective_type));
break;
case ContentSecurityPolicy::kTrustedTypesSinkViolation:
init->setBlockedURI("trusted-types-sink");
break;
case ContentSecurityPolicy::kTrustedTypesPolicyViolation:
init->setBlockedURI("trusted-types-policy");
break;
}
}
String effective_directive =
ContentSecurityPolicy::GetDirectiveName(effective_type);
init->setViolatedDirective(effective_directive);
init->setEffectiveDirective(effective_directive);
init->setOriginalPolicy(header);
init->setDisposition(header_type == ContentSecurityPolicyType::kEnforce
? "enforce"
: "report");
init->setStatusCode(0);
// See https://w3c.github.io/webappsec-csp/#create-violation-for-global.
// Step 3. If global is a Window object, set violation’s referrer to global’s
// document's referrer. [spec text]
String referrer = delegate->GetDocumentReferrer();
if (referrer)
init->setReferrer(referrer);
// Step 4. Set violation’s status to the HTTP status code for the resource
// associated with violation’s global object. [spec text]
base::Optional<uint16_t> status_code = delegate->GetStatusCode();
if (status_code)
init->setStatusCode(*status_code);
// If no source location is provided, use the source location from the
// |delegate|.
// Step 2. If the user agent is currently executing script, and can extract a
// source file’s URL, line number, and column number from the global, set
// violation’s source file, line number, and column number accordingly.
// [spec text]
if (!source_location)
source_location = delegate->GetSourceLocation();
if (source_location && source_location->LineNumber()) {
KURL source_url = KURL(source_location->Url());
// The source file might be a script loaded from a redirect. Web browser
// usually tries to hide post-redirect information. The script might be
// cross-origin with the document, but also with other scripts. As a result,
// everything is cleared no matter the |source_url| origin.
// See https://crbug.com/1074317
//
// Note: The username, password and ref are stripped later below by
// StripURLForUseInReport(..)
source_url.SetQuery(String());
// The |source_url| is the URL of the script that triggered the CSP
// violation. It is the URL pre-redirect. So it is safe to expose it in
// reports without leaking any new informations to the document. See
// https://crrev.com/c/2187792.
String source_file =
StripURLForUseInReport(delegate->GetSecurityOrigin(), source_url,
RedirectStatus::kNoRedirect, effective_type);
init->setSourceFile(source_file);
init->setLineNumber(source_location->LineNumber());
init->setColumnNumber(source_location->ColumnNumber());
} else {
init->setSourceFile(String());
init->setLineNumber(0);
init->setColumnNumber(0);
}
// Build the sample string. CSP demands that the sample is restricted to
// 40 characters (kMaxSampleLength), to prevent inadvertent exfiltration of
// user data. For some use cases, we also have a sample prefix, which
// must not depend on user data and where we will apply the sample limit
// separately.
StringBuilder sample;
if (!sample_prefix.IsEmpty()) {
sample.Append(sample_prefix.StripWhiteSpace().Left(
ContentSecurityPolicy::kMaxSampleLength));
sample.Append("|");
}
if (!script_source.IsEmpty()) {
sample.Append(script_source.StripWhiteSpace().Left(
ContentSecurityPolicy::kMaxSampleLength));
}
if (!sample.IsEmpty())
init->setSample(sample.ToString());
return source_location;
}
} // namespace
void ContentSecurityPolicy::ReportViolation(
const String& directive_text,
CSPDirectiveName effective_type,
const String& console_message,
const KURL& blocked_url,
const Vector<String>& report_endpoints,
bool use_reporting_api,
const String& header,
ContentSecurityPolicyType header_type,
ContentSecurityPolicyViolationType violation_type,
std::unique_ptr<SourceLocation> source_location,
LocalFrame* context_frame,
RedirectStatus redirect_status,
Element* element,
const String& source,
const String& source_prefix) {
DCHECK(violation_type == kURLViolation || blocked_url.IsEmpty());
// TODO(lukasza): Support sending reports from OOPIFs -
// https://crbug.com/611232 (or move CSP child-src and frame-src checks to the
// browser process - see https://crbug.com/376522).
if (!delegate_ && !context_frame) {
DCHECK(effective_type == CSPDirectiveName::ChildSrc ||
effective_type == CSPDirectiveName::FrameSrc ||
effective_type == CSPDirectiveName::TrustedTypes ||
effective_type == CSPDirectiveName::RequireTrustedTypesFor);
return;
}
DCHECK(
(delegate_ && !context_frame) ||
((effective_type == CSPDirectiveName::FrameAncestors) && context_frame));
SecurityPolicyViolationEventInit* violation_data =
SecurityPolicyViolationEventInit::Create();
// If we're processing 'frame-ancestors', use the delegate for the
// |context_frame|'s document to gather data. Otherwise, use the policy's
// |delegate_|.
ContentSecurityPolicyDelegate* relevant_delegate =
context_frame
? &context_frame->DomWindow()->GetContentSecurityPolicyDelegate()
: delegate_.Get();
DCHECK(relevant_delegate);
// Let GatherSecurityPolicyViolationEventData decide which source location to
// report.
source_location = GatherSecurityPolicyViolationEventData(
violation_data, relevant_delegate, directive_text, effective_type,
blocked_url, header, redirect_status, header_type, violation_type,
std::move(source_location), source, source_prefix);
// TODO(mkwst): Obviously, we shouldn't hit this check, as extension-loaded
// resources should be allowed regardless. We apparently do, however, so
// we should at least stop spamming reporting endpoints. See
// https://crbug.com/524356 for detail.
if (!violation_data->sourceFile().IsEmpty() &&
ShouldBypassContentSecurityPolicy(KURL(violation_data->sourceFile()))) {
return;
}
PostViolationReport(violation_data, context_frame, report_endpoints,
use_reporting_api);
// Fire a violation event if we're working with a delegate (e.g. we're not
// processing 'frame-ancestors').
if (delegate_)
delegate_->DispatchViolationEvent(*violation_data, element);
ReportContentSecurityPolicyIssue(*violation_data, header_type, violation_type,
context_frame, element,
source_location.get());
}
void ContentSecurityPolicy::PostViolationReport(
const SecurityPolicyViolationEventInit* violation_data,
LocalFrame* context_frame,
const Vector<String>& report_endpoints,
bool use_reporting_api) {
// We need to be careful here when deciding what information to send to the
// report-uri. Currently, we send only the current document's URL and the
// directive that was violated. The document's URL is safe to send because
// it's the document itself that's requesting that it be sent. You could
// make an argument that we shouldn't send HTTPS document URLs to HTTP
// report-uris (for the same reasons that we supress the Referer in that
// case), but the Referer is sent implicitly whereas this request is only
// sent explicitly. As for which directive was violated, that's pretty
// harmless information.
//
// TODO(mkwst): This justification is BS. Insecure reports are mixed content,
// let's kill them. https://crbug.com/695363
auto csp_report = std::make_unique<JSONObject>();
csp_report->SetString("document-uri", violation_data->documentURI());
csp_report->SetString("referrer", violation_data->referrer());
csp_report->SetString("violated-directive",
violation_data->violatedDirective());
csp_report->SetString("effective-directive",
violation_data->effectiveDirective());
csp_report->SetString("original-policy", violation_data->originalPolicy());
csp_report->SetString("disposition", violation_data->disposition());
csp_report->SetString("blocked-uri", violation_data->blockedURI());
if (violation_data->lineNumber())
csp_report->SetInteger("line-number", violation_data->lineNumber());
if (violation_data->columnNumber())
csp_report->SetInteger("column-number", violation_data->columnNumber());
if (!violation_data->sourceFile().IsEmpty())
csp_report->SetString("source-file", violation_data->sourceFile());
csp_report->SetInteger("status-code", violation_data->statusCode());
csp_report->SetString("script-sample", violation_data->sample());
auto report_object = std::make_unique<JSONObject>();
report_object->SetObject("csp-report", std::move(csp_report));
String stringified_report = report_object->ToJSONString();
// Only POST unique reports to the external endpoint; repeated reports add no
// value on the server side, as they're indistinguishable. Note that we'll
// fire the DOM event for every violation, as the page has enough context to
// react in some reasonable way to each violation as it occurs.
if (ShouldSendViolationReport(stringified_report)) {
DidSendViolationReport(stringified_report);
// If we're processing 'frame-ancestors', use the delegate for the
// |context_frame|'s document to post violation report. Otherwise, use the
// policy's |delegate_|.
bool is_frame_ancestors_violation = !!context_frame;
ContentSecurityPolicyDelegate* relevant_delegate =
is_frame_ancestors_violation
? &context_frame->DomWindow()->GetContentSecurityPolicyDelegate()
: delegate_.Get();
DCHECK(relevant_delegate);
relevant_delegate->PostViolationReport(*violation_data, stringified_report,
is_frame_ancestors_violation,
report_endpoints, use_reporting_api);
}
}
void ContentSecurityPolicy::ReportMixedContent(const KURL& blocked_url,
RedirectStatus redirect_status) {
for (const auto& policy : policies_) {
if (policy->block_all_mixed_content) {
ReportViolation(GetDirectiveName(CSPDirectiveName::BlockAllMixedContent),
CSPDirectiveName::BlockAllMixedContent, String(),
blocked_url, policy->report_endpoints,
policy->use_reporting_api, policy->header->header_value,
policy->header->type,
ContentSecurityPolicy::kURLViolation,
std::unique_ptr<SourceLocation>(),
/*contextFrame=*/nullptr, redirect_status);
}
}
}
void ContentSecurityPolicy::ReportReportOnlyInMeta(const String& header) {
LogToConsole("The report-only Content Security Policy '" + header +
"' was delivered via a <meta> element, which is disallowed. The "
"policy has been ignored.");
}
void ContentSecurityPolicy::ReportMetaOutsideHead(const String& header) {
LogToConsole("The Content Security Policy '" + header +
"' was delivered via a <meta> element outside the document's "
"<head>, which is disallowed. The policy has been ignored.");
}
void ContentSecurityPolicy::ReportValueForEmptyDirective(const String& name,
const String& value) {
LogToConsole("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.");
}
void ContentSecurityPolicy::ReportMixedContentReportURI(
const String& endpoint) {
LogToConsole("The Content Security Policy directive specifies as endpoint '" +
endpoint +
"'. This endpoint will be ignored since it violates the policy "
"for Mixed Content.");
}
void ContentSecurityPolicy::ReportInvalidInReportOnly(const String& name) {
LogToConsole("The Content Security Policy directive '" + name +
"' is ignored when delivered in a report-only policy.");
}
void ContentSecurityPolicy::ReportInvalidDirectiveInMeta(
const String& directive) {
LogToConsole(
"Content Security Policies delivered via a <meta> element may not "
"contain the " +
directive + " directive.");
}
void ContentSecurityPolicy::ReportUnsupportedDirective(const String& name) {
static const char kAllow[] = "allow";
static const char kOptions[] = "options";
static const char kPolicyURI[] = "policy-uri";
static const char kPluginTypes[] = "plugin-types";
static const char kAllowMessage[] =
"The 'allow' directive has been replaced with 'default-src'. Please use "
"that directive instead, as 'allow' has no effect.";
static const char kOptionsMessage[] =
"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.";
static const char kPolicyURIMessage[] =
"The 'policy-uri' directive has been removed from the "
"specification. Please specify a complete policy via "
"the Content-Security-Policy header.";
static const char kPluginTypesMessage[] =
"The Content-Security-Policy directive 'plugin-types' has been removed "
"from the specification. "
"If you want to block plugins, consider specifying \"object-src 'none'\" "
"instead.";
String message =
"Unrecognized Content-Security-Policy directive '" + name + "'.\n";
mojom::ConsoleMessageLevel level = mojom::ConsoleMessageLevel::kError;
if (EqualIgnoringASCIICase(name, kAllow)) {
message = kAllowMessage;
} else if (EqualIgnoringASCIICase(name, kOptions)) {
message = kOptionsMessage;
} else if (EqualIgnoringASCIICase(name, kPolicyURI)) {
message = kPolicyURIMessage;
} else if (EqualIgnoringASCIICase(name, kPluginTypes)) {
message = kPluginTypesMessage;
} else if (GetDirectiveType(name) != CSPDirectiveName::Unknown) {
message = "The Content-Security-Policy directive '" + name +
"' is implemented behind a flag which is currently disabled.\n";
level = mojom::ConsoleMessageLevel::kInfo;
}
LogToConsole(message, level);
}
void ContentSecurityPolicy::ReportDirectiveAsSourceExpression(
const String& directive_name,
const String& source_expression) {
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)?";
LogToConsole(message);
}
void ContentSecurityPolicy::ReportDuplicateDirective(const String& name) {
String message =
"Ignoring duplicate Content-Security-Policy directive '" + name + "'.\n";
LogToConsole(message);
}
void ContentSecurityPolicy::ReportInvalidRequireTrustedTypesFor(
const String& require_trusted_types_for) {
String message;
if (require_trusted_types_for.IsNull()) {
message =
"'require-trusted-types-for' Content Security Policy directive is "
"empty; The directive has no effect.\n";
} else {
const char* hint = "";
if (require_trusted_types_for == "script" ||
require_trusted_types_for == "scripts" ||
require_trusted_types_for == "'scripts'") {
hint = " Did you mean 'script'?";
}
message =
"Invalid expression in 'require-trusted-types-for' "
"Content Security Policy directive: " +
require_trusted_types_for + "." + hint + "\n";
}
LogToConsole(message, mojom::ConsoleMessageLevel::kWarning);
}
void ContentSecurityPolicy::ReportInvalidSandboxFlags(
const String& invalid_flags) {
LogToConsole(
"Error while parsing the 'sandbox' Content Security Policy directive: " +
invalid_flags);
}
void ContentSecurityPolicy::ReportInvalidDirectiveValueCharacter(
const String& directive_name,
const String& value) {
String message =
"The value for Content Security Policy directive '" + directive_name +
"' contains an invalid character: '" + value +
"'. In a source expression, non-whitespace characters outside ASCII "
"0x21-0x7E must be Punycode-encoded, as described in RFC 3492 "
"(https://tools.ietf.org/html/rfc3492), if part of the hostname and "
"percent-encoded, as described in RFC 3986, section 2.1 "
"(http://tools.ietf.org/html/rfc3986#section-2.1), if part of the path.";
LogToConsole(message);
}
void ContentSecurityPolicy::ReportInvalidPathCharacter(
const String& directive_name,
const String& value,
const char invalid_char) {
DCHECK(invalid_char == '#' || invalid_char == '?');
String ignoring =
"The fragment identifier, including the '#', will be ignored.";
if (invalid_char == '?')
ignoring = "The query component, including the '?', will be ignored.";
String message = "The source list for Content Security Policy directive '" +
directive_name +
"' contains a source with an invalid path: '" + value +
"'. " + ignoring;
LogToConsole(message);
}
void ContentSecurityPolicy::ReportInvalidSourceExpression(
const String& directive_name,
const String& source) {
String message = "The source list for Content Security Policy directive '" +
directive_name + "' contains an invalid source: '" + source +
"'. It will be ignored.";
if (EqualIgnoringASCIICase(source, "'none'"))
message = message +
" Note that 'none' has no effect unless it is the only "
"expression in the source list.";
LogToConsole(message);
}
void ContentSecurityPolicy::ReportMultipleReportToEndpoints() {
LogToConsole(
"The Content Security Policy directive 'report-to' contains more than "
"one endpoint. Only the first one will be used, the other ones will be "
"ignored.");
}
void ContentSecurityPolicy::LogToConsole(const String& message,
mojom::ConsoleMessageLevel level) {
LogToConsole(MakeGarbageCollected<ConsoleMessage>(
mojom::ConsoleMessageSource::kSecurity, level, message));
}
mojom::blink::ContentSecurityPolicyViolationType
ContentSecurityPolicy::BuildCSPViolationType(
ContentSecurityPolicy::ContentSecurityPolicyViolationType violation_type) {
switch (violation_type) {
case blink::ContentSecurityPolicy::ContentSecurityPolicyViolationType::
kEvalViolation:
return mojom::blink::ContentSecurityPolicyViolationType::kEvalViolation;
case blink::ContentSecurityPolicy::ContentSecurityPolicyViolationType::
kInlineViolation:
return mojom::blink::ContentSecurityPolicyViolationType::kInlineViolation;
case blink::ContentSecurityPolicy::ContentSecurityPolicyViolationType::
kTrustedTypesPolicyViolation:
return mojom::blink::ContentSecurityPolicyViolationType::
kTrustedTypesPolicyViolation;
case blink::ContentSecurityPolicy::ContentSecurityPolicyViolationType::
kTrustedTypesSinkViolation:
return mojom::blink::ContentSecurityPolicyViolationType::
kTrustedTypesSinkViolation;
case blink::ContentSecurityPolicy::ContentSecurityPolicyViolationType::
kURLViolation:
return mojom::blink::ContentSecurityPolicyViolationType::kURLViolation;
}
}
void ContentSecurityPolicy::ReportContentSecurityPolicyIssue(
const blink::SecurityPolicyViolationEventInit& violation_data,
ContentSecurityPolicyType header_type,
ContentSecurityPolicyViolationType violation_type,
LocalFrame* frame_ancestor,
Element* element,
SourceLocation* source_location) {
auto cspDetails = mojom::blink::ContentSecurityPolicyIssueDetails::New();
cspDetails->is_report_only =
header_type == ContentSecurityPolicyType::kReport;
if (violation_type == ContentSecurityPolicyViolationType::kURLViolation ||
violation_data.violatedDirective() == "frame-ancestors") {
cspDetails->blocked_url = KURL(violation_data.blockedURI());
}
cspDetails->violated_directive = violation_data.violatedDirective();
cspDetails->content_security_policy_violation_type =
BuildCSPViolationType(violation_type);
if (frame_ancestor) {
auto affected_frame = mojom::blink::AffectedFrame::New();
affected_frame->frame_id =
frame_ancestor->GetDevToolsFrameToken().ToString().c_str();
cspDetails->frame_ancestor = std::move(affected_frame);
}
if (violation_data.sourceFile() && violation_data.lineNumber()) {
auto affected_location = mojom::blink::AffectedLocation::New();
affected_location->url = violation_data.sourceFile();
// The frontend expects 0-based line numbers.
affected_location->line = violation_data.lineNumber() - 1;
affected_location->column = violation_data.columnNumber();
if (source_location) {
affected_location->script_id =
String::Number(source_location->ScriptId());
}
cspDetails->affected_location = std::move(affected_location);
}
if (element) {
cspDetails->violating_node_id = DOMNodeIds::IdForNode(element);
}
auto details = mojom::blink::InspectorIssueDetails::New();
details->csp_issue_details = std::move(cspDetails);
mojom::blink::InspectorIssueInfoPtr info =
mojom::blink::InspectorIssueInfo::New(
mojom::blink::InspectorIssueCode::kContentSecurityPolicyIssue,
std::move(details));
if (frame_ancestor)
frame_ancestor->AddInspectorIssue(std::move(info));
else if (delegate_)
delegate_->AddInspectorIssue(std::move(info));
}
void ContentSecurityPolicy::LogToConsole(ConsoleMessage* console_message,
LocalFrame* frame) {
if (frame)
frame->DomWindow()->AddConsoleMessage(console_message);
else if (delegate_)
delegate_->AddConsoleMessage(console_message);
else
console_messages_.push_back(console_message);
}
void ContentSecurityPolicy::ReportBlockedScriptExecutionToInspector(
const String& directive_text) const {
if (delegate_)
delegate_->ReportBlockedScriptExecutionToInspector(directive_text);
}
bool ContentSecurityPolicy::ExperimentalFeaturesEnabled() const {
return RuntimeEnabledFeatures::
ExperimentalContentSecurityPolicyFeaturesEnabled();
}
bool ContentSecurityPolicy::ShouldSendCSPHeader(ResourceType type) const {
// TODO(mkwst): Revisit this once the CORS prefetch issue with the 'CSP'
// header is worked out, one way or another:
// https://github.com/whatwg/fetch/issues/52
return false;
}
// static
bool ContentSecurityPolicy::ShouldBypassMainWorld(
const ExecutionContext* context) {
if (!context)
return false;
return ShouldBypassMainWorld(context->GetCurrentWorld().get());
}
// static
bool ContentSecurityPolicy::ShouldBypassMainWorld(
const DOMWrapperWorld* world) {
if (!world || !world->IsIsolatedWorld())
return false;
return IsolatedWorldCSP::Get().HasContentSecurityPolicy(world->GetWorldId());
}
bool ContentSecurityPolicy::ShouldSendViolationReport(
const String& report) const {
// Collisions have no security impact, so we can save space by storing only
// the string's hash rather than the whole report.
return !violation_reports_sent_.Contains(report.Impl()->GetHash());
}
void ContentSecurityPolicy::DidSendViolationReport(const String& report) {
violation_reports_sent_.insert(report.Impl()->GetHash());
}
const char* ContentSecurityPolicy::GetDirectiveName(CSPDirectiveName type) {
switch (type) {
case CSPDirectiveName::BaseURI:
return "base-uri";
case CSPDirectiveName::BlockAllMixedContent:
return "block-all-mixed-content";
case CSPDirectiveName::ChildSrc:
return "child-src";
case CSPDirectiveName::ConnectSrc:
return "connect-src";
case CSPDirectiveName::DefaultSrc:
return "default-src";
case CSPDirectiveName::FontSrc:
return "font-src";
case CSPDirectiveName::FormAction:
return "form-action";
case CSPDirectiveName::FrameAncestors:
return "frame-ancestors";
case CSPDirectiveName::FrameSrc:
return "frame-src";
case CSPDirectiveName::ImgSrc:
return "img-src";
case CSPDirectiveName::ManifestSrc:
return "manifest-src";
case CSPDirectiveName::MediaSrc:
return "media-src";
case CSPDirectiveName::NavigateTo:
return "navigate-to";
case CSPDirectiveName::ObjectSrc:
return "object-src";
case CSPDirectiveName::PrefetchSrc:
return "prefetch-src";
case CSPDirectiveName::ReportTo:
return "report-to";
case CSPDirectiveName::ReportURI:
return "report-uri";
case CSPDirectiveName::RequireTrustedTypesFor:
return "require-trusted-types-for";
case CSPDirectiveName::Sandbox:
return "sandbox";
case CSPDirectiveName::ScriptSrc:
return "script-src";
case CSPDirectiveName::ScriptSrcAttr:
return "script-src-attr";
case CSPDirectiveName::ScriptSrcElem:
return "script-src-elem";
case CSPDirectiveName::StyleSrc:
return "style-src";
case CSPDirectiveName::StyleSrcAttr:
return "style-src-attr";
case CSPDirectiveName::StyleSrcElem:
return "style-src-elem";
case CSPDirectiveName::TreatAsPublicAddress:
return "treat-as-public-address";
case CSPDirectiveName::TrustedTypes:
return "trusted-types";
case CSPDirectiveName::UpgradeInsecureRequests:
return "upgrade-insecure-requests";
case CSPDirectiveName::WorkerSrc:
return "worker-src";
case CSPDirectiveName::Unknown:
NOTREACHED();
return "";
}
NOTREACHED();
return "";
}
CSPDirectiveName ContentSecurityPolicy::GetDirectiveType(const String& name) {
if (name == "base-uri")
return CSPDirectiveName::BaseURI;
if (name == "block-all-mixed-content")
return CSPDirectiveName::BlockAllMixedContent;
if (name == "child-src")
return CSPDirectiveName::ChildSrc;
if (name == "connect-src")
return CSPDirectiveName::ConnectSrc;
if (name == "default-src")
return CSPDirectiveName::DefaultSrc;
if (name == "font-src")
return CSPDirectiveName::FontSrc;
if (name == "form-action")
return CSPDirectiveName::FormAction;
if (name == "frame-ancestors")
return CSPDirectiveName::FrameAncestors;
if (name == "frame-src")
return CSPDirectiveName::FrameSrc;
if (name == "img-src")
return CSPDirectiveName::ImgSrc;
if (name == "manifest-src")
return CSPDirectiveName::ManifestSrc;
if (name == "media-src")
return CSPDirectiveName::MediaSrc;
if (name == "navigate-to")
return CSPDirectiveName::NavigateTo;
if (name == "object-src")
return CSPDirectiveName::ObjectSrc;
if (name == "prefetch-src")
return CSPDirectiveName::PrefetchSrc;
if (name == "report-to")
return CSPDirectiveName::ReportTo;
if (name == "report-uri")
return CSPDirectiveName::ReportURI;
if (name == "require-trusted-types-for")
return CSPDirectiveName::RequireTrustedTypesFor;
if (name == "sandbox")
return CSPDirectiveName::Sandbox;
if (name == "script-src")
return CSPDirectiveName::ScriptSrc;
if (name == "script-src-attr")
return CSPDirectiveName::ScriptSrcAttr;
if (name == "script-src-elem")
return CSPDirectiveName::ScriptSrcElem;
if (name == "style-src")
return CSPDirectiveName::StyleSrc;
if (name == "style-src-attr")
return CSPDirectiveName::StyleSrcAttr;
if (name == "style-src-elem")
return CSPDirectiveName::StyleSrcElem;
if (name == "treat-as-public-address")
return CSPDirectiveName::TreatAsPublicAddress;
if (name == "trusted-types")
return CSPDirectiveName::TrustedTypes;
if (name == "upgrade-insecure-requests")
return CSPDirectiveName::UpgradeInsecureRequests;
if (name == "worker-src")
return CSPDirectiveName::WorkerSrc;
return CSPDirectiveName::Unknown;
}
bool ContentSecurityPolicy::ShouldBypassContentSecurityPolicy(
const KURL& url,
SchemeRegistry::PolicyAreas area) const {
bool should_bypass_csp;
if (SecurityOrigin::ShouldUseInnerURL(url)) {
should_bypass_csp = SchemeRegistry::SchemeShouldBypassContentSecurityPolicy(
SecurityOrigin::ExtractInnerURL(url).Protocol(), area);
if (should_bypass_csp) {
Count(WebFeature::kInnerSchemeBypassesCSP);
}
} else {
should_bypass_csp = SchemeRegistry::SchemeShouldBypassContentSecurityPolicy(
url.Protocol(), area);
}
if (should_bypass_csp) {
Count(WebFeature::kSchemeBypassesCSP);
}
return should_bypass_csp;
}
const WTF::Vector<network::mojom::blink::ContentSecurityPolicyPtr>&
ContentSecurityPolicy::GetParsedPolicies() const {
return policies_;
}
bool ContentSecurityPolicy::HasPolicyFromSource(
ContentSecurityPolicySource source) const {
for (const auto& policy : policies_) {
if (policy->header->source == source)
return true;
}
return false;
}
void ContentSecurityPolicy::Count(WebFeature feature) const {
if (delegate_)
delegate_->Count(feature);
}
} // namespace blink