blob: 13778c6000b93bfff921b717643b96e0c92518fb [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/renderer_host/ancestor_throttle.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/ranges/algorithm.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "content/browser/renderer_host/frame_tree.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/navigation_request.h"
#include "content/browser/renderer_host/navigator.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/renderer_host/render_view_host_delegate.h"
#include "content/browser/renderer_host/render_view_host_impl.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/navigation_throttle.h"
#include "content/public/browser/storage_partition.h"
#include "net/http/http_response_headers.h"
#include "services/network/public/cpp/content_security_policy/csp_context.h"
#include "services/network/public/cpp/features.h"
#include "third_party/blink/public/mojom/devtools/console_message.mojom.h"
namespace content {
namespace {
const char kXFrameOptionsSameOriginHistogram[] = "Security.XFrameOptions";
// This enum is used for UMA metrics. Keep these enums up to date with
// tools/metrics/histograms/histograms.xml.
enum XFrameOptionsHistogram {
// A frame is loaded without any X-Frame-Options header.
NONE = 0,
// X-Frame-Options: DENY.
DENY = 1,
// X-Frame-Options: SAMEORIGIN. The navigation proceeds and every ancestor
// has the same origin.
SAMEORIGIN = 2,
// X-Frame-Options: SAMEORIGIN. The navigation is blocked because the
// top-frame doesn't have the same origin.
SAMEORIGIN_BLOCKED = 3,
// X-Frame-Options: SAMEORIGIN. The navigation proceeds despite the fact that
// there is an ancestor that doesn't have the same origin.
SAMEORIGIN_WITH_BAD_ANCESTOR_CHAIN = 4,
// X-Frame-Options: ALLOWALL.
ALLOWALL = 5,
// Invalid 'X-Frame-Options' directive encountered.
INVALID = 6,
// The frame sets multiple 'X-Frame-Options' header with conflicting values.
CONFLICT = 7,
// The 'frame-ancestors' CSP directive should take effect instead.
BYPASS = 8,
// Navigation would have been blocked if we applied 'X-Frame-Options' to
// redirects.
//
// TODO(mkwst): Rename this when we make a decision around
// https://crbug.com/835465.
REDIRECT_WOULD_BE_BLOCKED = 9,
XFRAMEOPTIONS_HISTOGRAM_MAX = REDIRECT_WOULD_BE_BLOCKED
};
void RecordXFrameOptionsUsage(XFrameOptionsHistogram usage) {
UMA_HISTOGRAM_ENUMERATION(
kXFrameOptionsSameOriginHistogram, usage,
XFrameOptionsHistogram::XFRAMEOPTIONS_HISTOGRAM_MAX);
}
bool HeadersContainFrameAncestorsCSP(
const network::mojom::ParsedHeadersPtr& headers) {
return base::ranges::any_of(
headers->content_security_policy, [](const auto& csp) {
return csp->header->type ==
network::mojom::ContentSecurityPolicyType::kEnforce &&
csp->directives.count(
network::mojom::CSPDirectiveName::FrameAncestors);
});
}
class FrameAncestorCSPContext : public network::CSPContext {
public:
FrameAncestorCSPContext(
RenderFrameHostImpl* navigated_frame,
const std::vector<network::mojom::ContentSecurityPolicyPtr>& policies)
: navigated_frame_(navigated_frame) {
// TODO(arthursonzogni): Refactor CSPContext to its original state, it
// shouldn't own any ContentSecurityPolicies on its own. This should be
// defined by the implementation instead. Copies could be avoided here.
for (auto& policy : policies)
AddContentSecurityPolicy(mojo::Clone(policy));
}
private:
void ReportContentSecurityPolicyViolation(
network::mojom::CSPViolationPtr violation_params) override {
return navigated_frame_->ReportContentSecurityPolicyViolation(
std::move(violation_params));
}
bool SchemeShouldBypassCSP(const base::StringPiece& scheme) override {
return navigated_frame_->SchemeShouldBypassCSP(scheme);
}
void SanitizeDataForUseInCspViolation(
bool is_redirect,
network::mojom::CSPDirectiveName directive,
GURL* blocked_url,
network::mojom::SourceLocation* source_location) const override {
return navigated_frame_->SanitizeDataForUseInCspViolation(
is_redirect, directive, blocked_url, source_location);
}
RenderFrameHostImpl* navigated_frame_;
};
// Returns the parent, including outer delegates in the case of portals.
RenderFrameHostImpl* ParentOrOuterDelegate(RenderFrameHostImpl* frame) {
return frame->InsidePortal() ? frame->ParentOrOuterDelegateFrame()
: frame->GetParent();
}
} // namespace
// static
std::unique_ptr<NavigationThrottle> AncestorThrottle::MaybeCreateThrottleFor(
NavigationHandle* handle) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
return base::WrapUnique(new AncestorThrottle(handle));
}
AncestorThrottle::~AncestorThrottle() {}
NavigationThrottle::ThrottleCheckResult AncestorThrottle::WillStartRequest() {
if (!base::FeatureList::IsEnabled(network::features::kOutOfBlinkCSPEE))
return NavigationThrottle::PROCEED;
NavigationRequest* request = NavigationRequest::From(navigation_handle());
if (request->IsInMainFrame())
return NavigationThrottle::PROCEED;
// TODO(antoniosartori): Probably we should have taken a snapshot of the 'csp'
// attribute at the beginning of the navigation and not now, since the
// beforeunload handlers might have modified it in the meantime.
std::vector<network::mojom::ContentSecurityPolicyPtr> frame_csp;
frame_csp.emplace_back(
request->frame_tree_node()->csp_attribute()
? request->frame_tree_node()->csp_attribute()->Clone()
: nullptr);
const network::mojom::ContentSecurityPolicy* parent_required_csp =
request->frame_tree_node()->parent()->required_csp();
std::string error_message;
if (!network::IsValidRequiredCSPAttr(
frame_csp, parent_required_csp,
url::Origin::Create(navigation_handle()->GetURL()), error_message)) {
if (frame_csp[0]) {
navigation_handle()->GetParentFrame()->AddMessageToConsole(
blink::mojom::ConsoleMessageLevel::kError,
base::StringPrintf("The frame 'csp' attribute ('%s') is invalid and "
"will be discarded: %s",
frame_csp[0]->header->header_value.c_str(),
error_message.c_str()));
}
if (parent_required_csp)
request->SetRequiredCSP(parent_required_csp->Clone());
// TODO(antoniosartori): Consider instead blocking the navigation here,
// since this seems to be insecure
// (cf. https://github.com/w3c/webappsec-cspee/pull/11).
} else {
// If |frame_csp| is valid then it is not null.
request->SetRequiredCSP(std::move(frame_csp[0]));
}
return NavigationThrottle::PROCEED;
}
NavigationThrottle::ThrottleCheckResult
AncestorThrottle::WillRedirectRequest() {
// During a redirect, we don't know which RenderFrameHost we'll end up in,
// so we can't log reliably to the console. We should be able to work around
// this iff we decide to ship the redirect-blocking behavior, but for now
// we'll just skip the console-logging bits to collect metrics.
NavigationThrottle::ThrottleCheckResult result = ProcessResponseImpl(
LoggingDisposition::DO_NOT_LOG_TO_CONSOLE, false /* is_response_check */);
if (result.action() == NavigationThrottle::BLOCK_RESPONSE)
RecordXFrameOptionsUsage(XFrameOptionsHistogram::REDIRECT_WOULD_BE_BLOCKED);
// TODO(mkwst): We need to decide whether we'll be able to get away with
// tightening the XFO check to include redirect responses once we have a
// feel for the REDIRECT_WOULD_BE_BLOCKED numbers we're collecting above.
// Until then, we'll allow the response to proceed: https://crbug.com/835465.
return NavigationThrottle::PROCEED;
}
NavigationThrottle::ThrottleCheckResult
AncestorThrottle::WillProcessResponse() {
return ProcessResponseImpl(LoggingDisposition::LOG_TO_CONSOLE,
true /* is_response_check */);
}
NavigationThrottle::ThrottleCheckResult AncestorThrottle::ProcessResponseImpl(
LoggingDisposition logging,
bool is_response_check) {
NavigationRequest* request = NavigationRequest::From(navigation_handle());
bool is_portal =
request->frame_tree_node()->current_frame_host()->InsidePortal();
if (request->IsInMainFrame() && !is_portal) {
// Allow main frame navigations.
return NavigationThrottle::PROCEED;
}
// 204/205 responses and downloads are not sent to the renderer and don't need
// to be checked.
if (is_response_check && !request->response_should_be_rendered()) {
return NavigationThrottle::PROCEED;
}
const std::vector<network::mojom::ContentSecurityPolicyPtr>&
content_security_policies =
request->response()->parsed_headers->content_security_policy;
// CSP: frame-ancestors is checked only for the final response.
if (is_response_check &&
EvaluateFrameAncestors(content_security_policies) == CheckResult::BLOCK) {
return NavigationThrottle::BLOCK_RESPONSE;
}
if (EvaluateXFrameOptions(logging) == CheckResult::BLOCK)
return NavigationThrottle::BLOCK_RESPONSE;
// CSPEE is checked only for the final response.
if (is_response_check &&
EvaluateCSPEmbeddedEnforcement() == CheckResult::BLOCK) {
return NavigationThrottle::BLOCK_RESPONSE;
}
return NavigationThrottle::PROCEED;
}
const char* AncestorThrottle::GetNameForLogging() {
return "AncestorThrottle";
}
AncestorThrottle::AncestorThrottle(NavigationHandle* handle)
: NavigationThrottle(handle) {}
void AncestorThrottle::ParseXFrameOptionsError(const std::string& value,
HeaderDisposition disposition) {
DCHECK(disposition == HeaderDisposition::CONFLICT ||
disposition == HeaderDisposition::INVALID);
if (!navigation_handle()->GetRenderFrameHost())
return; // Some responses won't have a RFH (i.e. 204/205s or downloads).
std::string message;
if (disposition == HeaderDisposition::CONFLICT) {
message = base::StringPrintf(
"Refused to display '%s' in a frame because it set multiple "
"'X-Frame-Options' headers with conflicting values "
"('%s'). Falling back to 'deny'.",
navigation_handle()->GetURL().spec().c_str(), value.c_str());
} else {
message = base::StringPrintf(
"Invalid 'X-Frame-Options' header encountered when loading '%s': "
"'%s' is not a recognized directive. The header will be ignored.",
navigation_handle()->GetURL().spec().c_str(), value.c_str());
}
// Log a console error in the parent of the current RenderFrameHost (as
// the current RenderFrameHost itself doesn't yet have a document).
auto* frame = static_cast<RenderFrameHostImpl*>(
navigation_handle()->GetRenderFrameHost());
ParentOrOuterDelegate(frame)->AddMessageToConsole(
blink::mojom::ConsoleMessageLevel::kError, message);
}
void AncestorThrottle::ConsoleErrorXFrameOptions(
HeaderDisposition disposition) {
DCHECK(disposition == HeaderDisposition::DENY ||
disposition == HeaderDisposition::SAMEORIGIN);
if (!navigation_handle()->GetRenderFrameHost())
return; // Some responses won't have a RFH (i.e. 204/205s or downloads).
std::string message = base::StringPrintf(
"Refused to display '%s' in a frame because it set 'X-Frame-Options' "
"to '%s'.",
navigation_handle()->GetURL().spec().c_str(),
disposition == HeaderDisposition::DENY ? "deny" : "sameorigin");
// Log a console error in the parent of the current RenderFrameHost (as
// the current RenderFrameHost itself doesn't yet have a document).
auto* frame = static_cast<RenderFrameHostImpl*>(
navigation_handle()->GetRenderFrameHost());
ParentOrOuterDelegate(frame)->AddMessageToConsole(
blink::mojom::ConsoleMessageLevel::kError, message);
}
AncestorThrottle::CheckResult AncestorThrottle::EvaluateXFrameOptions(
LoggingDisposition logging) {
std::string header_value;
NavigationRequest* request = NavigationRequest::From(navigation_handle());
HeaderDisposition disposition =
ParseXFrameOptionsHeader(request->GetResponseHeaders(), &header_value);
// If 'X-Frame-Options' would potentially block the response, check whether
// the 'frame-ancestors' CSP directive should take effect instead. See
// https://www.w3.org/TR/CSP/#frame-ancestors-and-frame-options
if (disposition != HeaderDisposition::NONE &&
disposition != HeaderDisposition::ALLOWALL &&
HeadersContainFrameAncestorsCSP(request->response()->parsed_headers)) {
RecordXFrameOptionsUsage(XFrameOptionsHistogram::BYPASS);
return CheckResult::PROCEED;
}
switch (disposition) {
case HeaderDisposition::CONFLICT:
if (logging == LoggingDisposition::LOG_TO_CONSOLE)
ParseXFrameOptionsError(header_value, disposition);
RecordXFrameOptionsUsage(XFrameOptionsHistogram::CONFLICT);
return CheckResult::BLOCK;
case HeaderDisposition::INVALID:
if (logging == LoggingDisposition::LOG_TO_CONSOLE)
ParseXFrameOptionsError(header_value, disposition);
RecordXFrameOptionsUsage(XFrameOptionsHistogram::INVALID);
// TODO(mkwst): Consider failing here.
return CheckResult::PROCEED;
case HeaderDisposition::DENY:
if (logging == LoggingDisposition::LOG_TO_CONSOLE)
ConsoleErrorXFrameOptions(disposition);
RecordXFrameOptionsUsage(XFrameOptionsHistogram::DENY);
return CheckResult::BLOCK;
case HeaderDisposition::SAMEORIGIN: {
// Block the request when any ancestor is not same-origin.
RenderFrameHostImpl* parent = ParentOrOuterDelegate(
request->frame_tree_node()->current_frame_host());
url::Origin current_origin =
url::Origin::Create(navigation_handle()->GetURL());
while (parent) {
if (!parent->GetLastCommittedOrigin().IsSameOriginWith(
current_origin)) {
RecordXFrameOptionsUsage(XFrameOptionsHistogram::SAMEORIGIN_BLOCKED);
if (logging == LoggingDisposition::LOG_TO_CONSOLE)
ConsoleErrorXFrameOptions(disposition);
// TODO(mkwst): Stop recording this metric once we convince other
// vendors to follow our lead with XFO: SAMEORIGIN processing.
//
// https://crbug.com/250309
if (parent->GetMainFrame()->GetLastCommittedOrigin().IsSameOriginWith(
current_origin)) {
RecordXFrameOptionsUsage(
XFrameOptionsHistogram::SAMEORIGIN_WITH_BAD_ANCESTOR_CHAIN);
}
return CheckResult::BLOCK;
}
parent = ParentOrOuterDelegate(parent);
}
RecordXFrameOptionsUsage(XFrameOptionsHistogram::SAMEORIGIN);
return CheckResult::PROCEED;
}
case HeaderDisposition::NONE:
RecordXFrameOptionsUsage(XFrameOptionsHistogram::NONE);
return CheckResult::PROCEED;
case HeaderDisposition::ALLOWALL:
RecordXFrameOptionsUsage(XFrameOptionsHistogram::ALLOWALL);
return CheckResult::PROCEED;
}
}
AncestorThrottle::CheckResult AncestorThrottle::EvaluateFrameAncestors(
const std::vector<network::mojom::ContentSecurityPolicyPtr>&
content_security_policy) {
// TODO(lfg): If the initiating document is known and correspond to the
// navigating frame's current document, consider using:
// navigation_request().common_params().source_location here instead.
auto empty_source_location = network::mojom::SourceLocation::New();
// CSP frame-ancestors are checked against the URL of every parent and are
// reported to the navigating frame.
FrameAncestorCSPContext csp_context(
NavigationRequest::From(navigation_handle())->GetRenderFrameHost(),
content_security_policy);
csp_context.SetSelf(url::Origin::Create(navigation_handle()->GetURL()));
// Check CSP frame-ancestors against every parent.
// We enforce frame-ancestors in the outer delegate for portals, but not
// for other uses of inner/outer WebContents (GuestViews).
RenderFrameHostImpl* parent =
ParentOrOuterDelegate(static_cast<RenderFrameHostImpl*>(
navigation_handle()->GetRenderFrameHost()));
while (parent) {
if (!csp_context.IsAllowedByCsp(
network::mojom::CSPDirectiveName::FrameAncestors,
parent->GetLastCommittedOrigin().GetURL(),
navigation_handle()->WasServerRedirect(),
true /* is_response_check */, empty_source_location,
network::CSPContext::CheckCSPDisposition::CHECK_ALL_CSP,
navigation_handle()->IsFormSubmission())) {
return CheckResult::BLOCK;
}
parent = ParentOrOuterDelegate(parent);
}
return CheckResult::PROCEED;
}
// When the embedder requires the use of Content Security Policy via Embedded
// Enforcement, framed documents must either
// 1) Use the 'allow-csp-from' header to opt-into enforcement.
// 2) Enforce its own CSP that subsumes the required CSP.
// Framed documents that fail to do either of these will be blocked.
//
// See:
// - https://w3c.github.io/webappsec-cspee/#required-csp-header
// - https://w3c.github.io/webappsec-cspee/#allow-csp-from-header
AncestorThrottle::CheckResult
AncestorThrottle::EvaluateCSPEmbeddedEnforcement() {
if (!base::FeatureList::IsEnabled(network::features::kOutOfBlinkCSPEE))
return CheckResult::PROCEED;
NavigationRequest* request = NavigationRequest::From(navigation_handle());
if (request->IsInMainFrame()) {
// We enforce CSPEE only for frames, not for portals.
return CheckResult::PROCEED;
}
RenderFrameHostImpl* frame = static_cast<RenderFrameHostImpl*>(
navigation_handle()->GetRenderFrameHost());
if (!request->required_csp())
return CheckResult::PROCEED;
const network::mojom::AllowCSPFromHeaderValuePtr& allow_csp_from =
request->response()->parsed_headers->allow_csp_from;
if (AllowsBlanketEnforcementOfRequiredCSP(
frame->GetParent()->GetLastCommittedOrigin(),
navigation_handle()->GetURL(), allow_csp_from)) {
// Enforce the required csps on the frame by passing them down to blink
request->ForceCSPForResponse(request->required_csp()->header->header_value);
return CheckResult::PROCEED;
}
std::string sanitized_blocked_url =
navigation_handle()->GetRedirectChain().front().GetOrigin().spec();
if (allow_csp_from && allow_csp_from->is_error_message()) {
frame->GetParent()->AddMessageToConsole(
blink::mojom::ConsoleMessageLevel::kError,
base::StringPrintf("The value of the 'Allow-CSP-From' response header "
"returned by %s is invalid: %s",
sanitized_blocked_url.c_str(),
allow_csp_from->get_error_message().c_str()));
}
if (network::Subsumes(
*request->required_csp(),
request->response()->parsed_headers->content_security_policy,
url::Origin::Create(navigation_handle()->GetURL()))) {
return CheckResult::PROCEED;
}
frame->GetParent()->AddMessageToConsole(
blink::mojom::ConsoleMessageLevel::kError,
base::StringPrintf(
"Refused to display '%s' in a frame. The embedder requires it to "
"enforce the following Content Security Policy: '%s'. However, the "
"frame neither accepts that policy using the Allow-CSP-From header "
"nor delivers a Content Security Policy which is at least as strong "
"as that one.",
sanitized_blocked_url.c_str(),
request->required_csp()->header->header_value.c_str()));
return CheckResult::BLOCK;
}
// static
bool AncestorThrottle::AllowsBlanketEnforcementOfRequiredCSP(
const url::Origin& request_origin,
const GURL& response_url,
const network::mojom::AllowCSPFromHeaderValuePtr& allow_csp_from) {
if (response_url.SchemeIs(url::kAboutScheme) ||
response_url.SchemeIs(url::kDataScheme) || response_url.SchemeIsFile() ||
response_url.SchemeIsFileSystem() || response_url.SchemeIsBlob()) {
return true;
}
if (request_origin.IsSameOriginWith(url::Origin::Create(response_url))) {
return true;
}
if (!allow_csp_from)
return false;
if (allow_csp_from->is_allow_star()) {
return true;
}
if (allow_csp_from->is_origin() &&
request_origin.IsSameOriginWith(allow_csp_from->get_origin())) {
return true;
}
return false;
}
AncestorThrottle::HeaderDisposition AncestorThrottle::ParseXFrameOptionsHeader(
const net::HttpResponseHeaders* headers,
std::string* header_value) {
DCHECK(header_value);
if (!headers)
return HeaderDisposition::NONE;
// Process the 'X-Frame-Options header as per Section 2 of RFC7034:
// https://tools.ietf.org/html/rfc7034#section-2
//
// Note that we do not support the 'ALLOW-FROM' value, and we special-case
// the invalid "ALLOWALL" value due to its prevalance in the wild.
HeaderDisposition result = HeaderDisposition::NONE;
size_t iter = 0;
std::string value;
while (headers->EnumerateHeader(&iter, "x-frame-options", &value)) {
HeaderDisposition current = HeaderDisposition::INVALID;
base::StringPiece trimmed =
base::TrimWhitespaceASCII(value, base::TRIM_ALL);
if (!header_value->empty())
header_value->append(", ");
header_value->append(trimmed.as_string());
if (base::LowerCaseEqualsASCII(trimmed, "deny"))
current = HeaderDisposition::DENY;
else if (base::LowerCaseEqualsASCII(trimmed, "allowall"))
current = HeaderDisposition::ALLOWALL;
else if (base::LowerCaseEqualsASCII(trimmed, "sameorigin"))
current = HeaderDisposition::SAMEORIGIN;
else
current = HeaderDisposition::INVALID;
if (result == HeaderDisposition::NONE)
result = current;
else if (result != current)
result = HeaderDisposition::CONFLICT;
}
return result;
}
} // namespace content