blob: b8da8508ad1d22af5e90ee7bb69bb8af04db69fb [file] [log] [blame]
// Copyright 2016 The Chromium Authors
// 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 <algorithm>
#include "base/feature_list.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.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_csp_context.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/common/features.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 "content/public/common/content_client.h"
#include "net/http/http_response_headers.h"
#include "services/network/public/cpp/content_security_policy/content_security_policy.h"
#include "services/network/public/cpp/content_security_policy/csp_context.h"
#include "third_party/blink/public/mojom/devtools/console_message.mojom-shared.h"
#include "third_party/blink/public/mojom/devtools/console_message.mojom.h"
#include "third_party/blink/public/mojom/use_counter/metrics/web_feature.mojom.h"
namespace content {
namespace {
bool HeadersContainFrameAncestorsCSP(
const network::mojom::ParsedHeadersPtr& headers) {
return std::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);
});
}
// From a RenderFrameHost |frame|, return its parent. This escapes FencedFrames
// that allow for information inflow, but does not escape nested WebContents.
// This returns nullptr for the top-level document and fenced frame roots (if
// information inflow is not allowed for the fenced frame). |request| is only
// supplied when |frame| is the frame being navigated. The FencedFrameProperties
// has not been installed in |frame|'s FrameTreeNode yet. Instead, we look at
// the one attached to the NavigationRequest.
RenderFrameHostImpl* GetParentForFrameAncestors(NavigationRequest* request,
RenderFrameHostImpl* frame) {
bool allows_information_inflow = false;
if (base::FeatureList::IsEnabled(
blink::features::kFencedFramesLocalUnpartitionedDataAccess)) {
if (request) {
allows_information_inflow =
!request->GetFencedFrameProperties().has_value() ||
request->GetFencedFrameProperties()->allows_information_inflow();
} else {
// We are in one of the navigated frame's ancestors.
allows_information_inflow =
!frame->frame_tree_node()->HasFencedFrameProperties() ||
frame->frame_tree_node()
->GetFencedFrameProperties()
->allows_information_inflow();
}
} else {
allows_information_inflow = !frame->IsFencedFrameRoot();
}
if (!allows_information_inflow && request &&
base::FeatureList::IsEnabled(
blink::features::kFencedFramesLocalUnpartitionedDataAccess)) {
request->AddDeferredConsoleMessage(
blink::mojom::ConsoleMessageLevel::kWarning,
"'CSP frame-ancestors' and 'X-Frame-Options' directives will not look "
"past a fenced frame boundary if created with an API that disallows "
"information inflow, such as Protected Audience.");
}
return allows_information_inflow ? frame->GetParentOrOuterDocument()
: nullptr;
}
} // namespace
// static
void AncestorThrottle::CreateAndAdd(NavigationThrottleRegistry& registry) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
registry.AddThrottle(base::WrapUnique(new AncestorThrottle(registry)));
}
AncestorThrottle::~AncestorThrottle() {}
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 */);
// 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());
if (request->IsInOutermostMainFrame()) {
// 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;
if (EvaluateEmbeddingOptIn(logging) == CheckResult::BLOCK)
return NavigationThrottle::BLOCK_RESPONSE;
return NavigationThrottle::PROCEED;
}
const char* AncestorThrottle::GetNameForLogging() {
return "AncestorThrottle";
}
AncestorThrottle::AncestorThrottle(NavigationThrottleRegistry& registry)
: NavigationThrottle(registry) {}
void AncestorThrottle::ParseXFrameOptionsError(
const net::HttpResponseHeaders* headers,
network::mojom::XFrameOptionsValue disposition) {
DCHECK(disposition == network::mojom::XFrameOptionsValue::kConflict ||
disposition == network::mojom::XFrameOptionsValue::kInvalid);
DCHECK(headers);
std::string value =
headers->GetNormalizedHeader("X-Frame-Options").value_or(std::string());
std::string message;
if (disposition == network::mojom::XFrameOptionsValue::kConflict) {
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'.",
url::Origin::Create(navigation_handle()->GetURL())
.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.",
url::Origin::Create(navigation_handle()->GetURL())
.GetURL()
.spec()
.c_str(),
value.c_str());
}
AddMessageToConsole(blink::mojom::ConsoleMessageLevel::kError,
std::move(message));
}
void AncestorThrottle::ConsoleErrorEmbeddingRequiresOptIn() {
DCHECK(base::FeatureList::IsEnabled(features::kEmbeddingRequiresOptIn));
std::string message = base::StringPrintf(
"Refused to display '%s' in a frame: It did not opt-into cross-origin "
"embedding by setting either an 'X-Frame-Options' header, or a "
"'Content-Security-Policy' header containing a 'frame-ancestors' "
"directive.",
url::Origin::Create(navigation_handle()->GetURL())
.GetURL()
.spec()
.c_str());
AddMessageToConsole(blink::mojom::ConsoleMessageLevel::kError,
std::move(message));
}
void AncestorThrottle::ConsoleErrorXFrameOptions(
network::mojom::XFrameOptionsValue disposition) {
DCHECK(disposition == network::mojom::XFrameOptionsValue::kDeny ||
disposition == network::mojom::XFrameOptionsValue::kSameOrigin);
std::string message = base::StringPrintf(
"Refused to display '%s' in a frame because it set 'X-Frame-Options' "
"to '%s'.",
url::Origin::Create(navigation_handle()->GetURL())
.GetURL()
.spec()
.c_str(),
disposition == network::mojom::XFrameOptionsValue::kDeny ? "deny"
: "sameorigin");
AddMessageToConsole(blink::mojom::ConsoleMessageLevel::kError,
std::move(message));
}
void AncestorThrottle::AddMessageToConsole(
blink::mojom::ConsoleMessageLevel level,
std::string message) {
NavigationRequest::From(navigation_handle())
->AddDeferredConsoleMessage(level, std::move(message));
}
AncestorThrottle::CheckResult AncestorThrottle::EvaluateXFrameOptions(
LoggingDisposition logging) {
NavigationRequest* request = NavigationRequest::From(navigation_handle());
network::mojom::XFrameOptionsValue disposition =
request->response()->parsed_headers->xfo;
// 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 != network::mojom::XFrameOptionsValue::kNone &&
disposition != network::mojom::XFrameOptionsValue::kAllowAll &&
HeadersContainFrameAncestorsCSP(request->response()->parsed_headers)) {
return CheckResult::PROCEED;
}
switch (disposition) {
case network::mojom::XFrameOptionsValue::kConflict:
if (logging == LoggingDisposition::LOG_TO_CONSOLE)
ParseXFrameOptionsError(request->GetResponseHeaders(), disposition);
return CheckResult::BLOCK;
case network::mojom::XFrameOptionsValue::kInvalid:
if (logging == LoggingDisposition::LOG_TO_CONSOLE)
ParseXFrameOptionsError(request->GetResponseHeaders(), disposition);
// TODO(mkwst): Consider failing here, especially if we end up shipping
// a new default behavior which requires embedees to explicitly opt-in
// to being embedded: https://crbug.com/1153274.
return CheckResult::PROCEED;
case network::mojom::XFrameOptionsValue::kDeny:
if (logging == LoggingDisposition::LOG_TO_CONSOLE)
ConsoleErrorXFrameOptions(disposition);
return CheckResult::BLOCK;
case network::mojom::XFrameOptionsValue::kSameOrigin: {
// Block the request when any ancestor is not same-origin.
// We enforce XFrameOptions in the outer documents, but not for
// embedders/GuestViews.
RenderFrameHostImpl* parent = GetParentForFrameAncestors(
request, 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)) {
if (logging == LoggingDisposition::LOG_TO_CONSOLE)
ConsoleErrorXFrameOptions(disposition);
return CheckResult::BLOCK;
}
parent = GetParentForFrameAncestors(nullptr, parent);
}
return CheckResult::PROCEED;
}
case network::mojom::XFrameOptionsValue::kNone:
return CheckResult::PROCEED;
case network::mojom::XFrameOptionsValue::kAllowAll:
return CheckResult::PROCEED;
}
}
AncestorThrottle::CheckResult AncestorThrottle::EvaluateEmbeddingOptIn(
LoggingDisposition logging) {
// If the proposal in https://github.com/mikewest/embedding-requires-opt-in is
// enabled, a response will be blocked unless it's explicitly opted-into
// being embeddable via 'X-Frame-Options'/'frame-ancestors', or is same-origin
// with its ancestors.
// We enforce frame-ancestors in the outer documents, but not for
// embedders/GuestViews.
NavigationRequest* request = NavigationRequest::From(navigation_handle());
if (request->response()->parsed_headers->xfo ==
network::mojom::XFrameOptionsValue::kNone &&
!HeadersContainFrameAncestorsCSP(request->response()->parsed_headers)) {
RenderFrameHostImpl* parent = GetParentForFrameAncestors(
request, request->frame_tree_node()->current_frame_host());
while (parent) {
if (!parent->GetLastCommittedOrigin().IsSameOriginWith(
navigation_handle()->GetURL())) {
GetContentClient()->browser()->LogWebFeatureForCurrentPage(
parent, blink::mojom::WebFeature::
kEmbeddedCrossOriginFrameWithoutFrameAncestorsOrXFO);
if (!base::FeatureList::IsEnabled(features::kEmbeddingRequiresOptIn))
return CheckResult::PROCEED;
if (logging == LoggingDisposition::LOG_TO_CONSOLE)
ConsoleErrorEmbeddingRequiresOptIn();
return CheckResult::BLOCK;
}
parent = GetParentForFrameAncestors(nullptr, parent);
}
}
return CheckResult::PROCEED;
}
AncestorThrottle::CheckResult AncestorThrottle::EvaluateFrameAncestors(
const std::vector<network::mojom::ContentSecurityPolicyPtr>&
content_security_policy) {
NavigationRequest* request = NavigationRequest::From(navigation_handle());
// 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();
// Check CSP frame-ancestors against every parent. We enforce frame-ancestors
// in the outer documents (except for fenced frames created under certain
// conditions), but not for embedders or GuestViews.
RenderFrameHostImpl* parent = GetParentForFrameAncestors(
request, static_cast<RenderFrameHostImpl*>(
navigation_handle()->GetRenderFrameHost()));
while (parent) {
// CSP violations (if any) are reported via the disallowed ancestor of the
// navigated frame (because while the throttle runs the navigation hasn't
// committed yet and the target frame might not yet have a URLLoaderFactory
// that could be used to report the violation).
// See also https://crbug.com/1111049.
network::CSPCheckResult result =
RenderFrameHostCSPContext(parent).IsAllowedByCsp(
content_security_policy,
network::mojom::CSPDirectiveName::FrameAncestors,
parent->GetLastCommittedOrigin().GetURL(),
GURL(), // url_before_redirects is ignored for frame-ancestors
navigation_handle()->WasServerRedirect(), empty_source_location,
network::CSPContext::CheckCSPDisposition::CHECK_ALL_CSP,
navigation_handle()->IsFormSubmission());
if (result.WouldBlockIfWildcardDoesNotMatchWs()) {
GetContentClient()->browser()->LogWebFeatureForCurrentPage(
parent,
blink::mojom::WebFeature::kCspWouldBlockIfWildcardDoesNotMatchWs);
}
if (!result) {
return CheckResult::BLOCK;
}
parent = GetParentForFrameAncestors(nullptr, parent);
}
return CheckResult::PROCEED;
}
} // namespace content