blob: 2832813f0fd157b68205b2a309ec791915ccbc40 [file] [log] [blame]
// Copyright 2021 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/navigation_policy_container_builder.h"
#include <utility>
#include "content/browser/renderer_host/frame_navigation_entry.h"
#include "content/browser/renderer_host/navigation_state_keep_alive.h"
#include "content/browser/renderer_host/policy_container_host.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/navigation_handle.h"
#include "services/network/public/cpp/web_sandbox_flags.h"
#include "services/network/public/mojom/content_security_policy.mojom-forward.h"
#include "services/network/public/mojom/ip_address_space.mojom.h"
#include "services/network/public/mojom/web_sandbox_flags.mojom.h"
namespace content {
namespace {
// Returns a copy of |parent|'s policies, or nullopt if |parent| is nullptr.
std::unique_ptr<PolicyContainerPolicies> GetParentPolicies(
RenderFrameHostImpl* parent) {
if (!parent) {
return nullptr;
}
return parent->policy_container_host()->policies().ClonePtr();
}
// Returns a copy of the navigation initiator's policies, if any.
//
// Must only be called on the browser's UI thread.
std::unique_ptr<PolicyContainerPolicies> GetInitiatorPolicies(
const blink::LocalFrameToken* frame_token,
int initiator_process_id,
StoragePartitionImpl* storage_partition) {
if (!frame_token) {
return nullptr;
}
PolicyContainerHost* initiator_policy_container_host =
RenderFrameHostImpl::GetPolicyContainerHost(
frame_token, initiator_process_id, storage_partition);
DCHECK(initiator_policy_container_host);
if (!initiator_policy_container_host) {
// Guard against wrong tokens being passed accidentally.
return nullptr;
}
return initiator_policy_container_host->policies().ClonePtr();
}
// Returns a copy of the given history |entry|'s policies, if any.
std::unique_ptr<PolicyContainerPolicies> GetHistoryPolicies(
const FrameNavigationEntry* entry) {
if (!entry) {
return nullptr;
}
const PolicyContainerPolicies* policies = entry->policy_container_policies();
if (!policies) {
return nullptr;
}
return policies->ClonePtr();
}
} // namespace
NavigationPolicyContainerBuilder::NavigationPolicyContainerBuilder(
RenderFrameHostImpl* parent,
const blink::LocalFrameToken* initiator_frame_token,
int initiator_process_id,
StoragePartition* storage_partition,
const FrameNavigationEntry* history_entry)
: parent_policies_(GetParentPolicies(parent)),
initiator_policies_(GetInitiatorPolicies(
initiator_frame_token,
initiator_process_id,
static_cast<StoragePartitionImpl*>(storage_partition))),
history_policies_(GetHistoryPolicies(history_entry)) {}
NavigationPolicyContainerBuilder::~NavigationPolicyContainerBuilder() = default;
const PolicyContainerPolicies*
NavigationPolicyContainerBuilder::InitiatorPolicies() const {
return initiator_policies_.get();
}
const PolicyContainerPolicies*
NavigationPolicyContainerBuilder::ParentPolicies() const {
return parent_policies_.get();
}
const PolicyContainerPolicies*
NavigationPolicyContainerBuilder::HistoryPolicies() const {
return history_policies_.get();
}
void NavigationPolicyContainerBuilder::SetIPAddressSpace(
network::mojom::IPAddressSpace address_space) {
DCHECK(!HasComputedPolicies());
delivered_policies_.ip_address_space = address_space;
}
void NavigationPolicyContainerBuilder::
SetLocalNetworkAccessNonSecureContextAllowed(bool allowed) {
DCHECK(!HasComputedPolicies());
delivered_policies_.allow_non_secure_local_network_access = allowed;
}
void NavigationPolicyContainerBuilder::SetIsOriginPotentiallyTrustworthy(
bool value) {
DCHECK(!HasComputedPolicies());
delivered_policies_.is_web_secure_context = value;
}
void NavigationPolicyContainerBuilder::SetCrossOriginIsolationEnabledByDIP() {
DCHECK(HasComputedPolicies());
host_->SetCrossOriginIsolationEnabledByDIP();
}
void NavigationPolicyContainerBuilder::AddContentSecurityPolicy(
network::mojom::ContentSecurityPolicyPtr policy) {
DCHECK(!HasComputedPolicies());
DCHECK(policy);
delivered_policies_.content_security_policies.push_back(std::move(policy));
}
void NavigationPolicyContainerBuilder::AddContentSecurityPolicies(
std::vector<network::mojom::ContentSecurityPolicyPtr> policies) {
DCHECK(!HasComputedPolicies());
delivered_policies_.AddContentSecurityPolicies(std::move(policies));
}
void NavigationPolicyContainerBuilder::SetCrossOriginOpenerPolicy(
network::CrossOriginOpenerPolicy coop) {
DCHECK(!HasComputedPolicies());
delivered_policies_.cross_origin_opener_policy = coop;
}
void NavigationPolicyContainerBuilder::SetCrossOriginEmbedderPolicy(
network::CrossOriginEmbedderPolicy coep) {
DCHECK(!HasComputedPolicies());
delivered_policies_.cross_origin_embedder_policy = coep;
}
void NavigationPolicyContainerBuilder::SetDocumentIsolationPolicy(
const network::DocumentIsolationPolicy& dip) {
DCHECK(!HasComputedPolicies());
delivered_policies_.document_isolation_policy = dip;
}
void NavigationPolicyContainerBuilder::SetIntegrityPolicy(
network::IntegrityPolicy ip) {
DCHECK(!HasComputedPolicies());
delivered_policies_.integrity_policy = std::move(ip);
}
void NavigationPolicyContainerBuilder::SetIntegrityPolicyReportOnly(
network::IntegrityPolicy ip) {
DCHECK(!HasComputedPolicies());
delivered_policies_.integrity_policy_report_only = std::move(ip);
}
const PolicyContainerPolicies&
NavigationPolicyContainerBuilder::DeliveredPoliciesForTesting() const {
DCHECK(!HasComputedPolicies());
return delivered_policies_;
}
void NavigationPolicyContainerBuilder::ComputePoliciesForError() {
// The decision to commit an error page can happen after receiving the
// response for a regular document. It overrides any previous attempt to
// |ComputePolicies()|.
host_ = nullptr;
DCHECK(!HasComputedPolicies());
// TODO(crbug.com/40747546): We should enforce strict policies on error
// pages.
PolicyContainerPolicies policies;
// We commit error pages with the same address space as the underlying page,
// so that auto-reloading error pages does not show up as a private network
// request (from the unknown/public address space to private). See also
// crbug.com/1180140.
policies.ip_address_space = delivered_policies_.ip_address_space;
SetFinalPolicies(std::move(policies));
DCHECK(HasComputedPolicies());
}
void NavigationPolicyContainerBuilder::ComputeIsWebSecureContext() {
DCHECK(!HasComputedPolicies());
if (!parent_policies_) {
// No parent. Only the trustworthiness of the origin matters.
return;
}
// The child can only be a secure context if the parent is too.
delivered_policies_.is_web_secure_context &=
parent_policies_->is_web_secure_context;
}
void NavigationPolicyContainerBuilder::ComputeSandboxFlags(
bool is_inside_mhtml,
network::mojom::WebSandboxFlags frame_sandbox_flags,
PolicyContainerPolicies& policies) {
DCHECK(!HasComputedPolicies());
auto sandbox_flags_to_commit = frame_sandbox_flags;
// The document can also restrict sandbox further, via its CSP.
for (const auto& csp : policies.content_security_policies) {
sandbox_flags_to_commit |= csp->sandbox;
}
// The URL of a document loaded from a MHTML archive is controlled by the
// Content-Location header. This can be set to an arbitrary URL. This is
// potentially dangerous. For this reason we force the document to be
// sandboxed, providing exceptions only for creating new windows. This
// includes disallowing javascript and using an opaque origin.
if (is_inside_mhtml) {
network::mojom::WebSandboxFlags allowed_flags =
network::mojom::WebSandboxFlags::kPopups |
network::mojom::WebSandboxFlags::kPropagatesToAuxiliaryBrowsingContexts;
// Allow JS to execute in saved MHTML documents, since certain constructs
// like custom elements, require additional JS to support. This is believed
// to be safe because:
// - MHTML serialization generally tries to drop script, though this is on
// a best-effort basis
// - a MHTML document and all its descendant frames are sandboxed without
// the allow-same-origin flag, so even though an MHTML archive can claim
// to contain resources from arbitrary URLs, each frame will have a
// unique opaque origin, which should limit any potential damage.
if (base::FeatureList::IsEnabled(blink::features::kMHTML_Improvements)) {
allowed_flags |= network::mojom::WebSandboxFlags::kScripts;
}
sandbox_flags_to_commit |= ~allowed_flags;
}
policies.sandbox_flags = sandbox_flags_to_commit;
}
void NavigationPolicyContainerBuilder::IncorporateDeliveredPoliciesForLocalURL(
PolicyContainerPolicies& policies) {
// Delivered content security policies must be appended.
policies.AddContentSecurityPolicies(
mojo::Clone(delivered_policies_.content_security_policies));
// The delivered IP address space (if any) overrides the IP address space.
if (delivered_policies_.ip_address_space !=
network::mojom::IPAddressSpace::kUnknown) {
policies.ip_address_space = delivered_policies_.ip_address_space;
}
}
PolicyContainerPolicies
NavigationPolicyContainerBuilder::ComputeInheritedPolicies(const GURL& url) {
DCHECK(url.SchemeIsLocal()) << url << " should not inherit policies";
if (url.IsAboutSrcdoc()) {
DCHECK(parent_policies_)
<< "About:srcdoc documents should always have a parent frame.";
return parent_policies_->Clone();
}
if (initiator_policies_) {
return initiator_policies_->Clone();
}
return PolicyContainerPolicies();
}
PolicyContainerPolicies NavigationPolicyContainerBuilder::ComputeFinalPolicies(
NavigationHandle* navigation_handle,
bool is_inside_mhtml,
network::mojom::WebSandboxFlags frame_sandbox_flags,
bool is_credentialless) {
PolicyContainerPolicies policies;
// Policies are either inherited from another document for local scheme, or
// directly set from the delivered response.
const GURL& url = navigation_handle->GetURL();
if (!url.SchemeIsLocal()) {
policies = delivered_policies_.Clone();
} else if (history_policies_) {
// For a local scheme, history policies should not incorporate delivered
// ones as this may lead to duplication of some policies already stored in
// history. For example, consider the following HTML:
// <iframe src="about:blank" csp="something">
// This will store CSP: something in history. The next time we have a
// history navigation we will have CSP: something twice.
policies = history_policies_->Clone();
} else {
policies = ComputeInheritedPolicies(url);
IncorporateDeliveredPoliciesForLocalURL(policies);
// TODO(crbug.com/40053796): Persist the policy container for URLs with
// local schemes so this override is not needed.
std::optional<network::CrossOriginEmbedderPolicy>
override_cross_origin_embedder_policy =
GetContentClient()
->browser()
->MaybeOverrideLocalURLCrossOriginEmbedderPolicy(
navigation_handle);
if (override_cross_origin_embedder_policy) {
policies.cross_origin_embedder_policy =
override_cross_origin_embedder_policy.value();
}
}
// `can_navigate_top_without_user_gesture` is inherited from the parent.
// Later in `NavigationRequest::CommitNavigation()` it will either be made
// less strict for same-origin navigations, or stricter for cross-origin
// navigations that do not explicitly allow top-level navigation without user
// gesture.
policies.can_navigate_top_without_user_gesture =
parent_policies_ ? parent_policies_->can_navigate_top_without_user_gesture
: true;
ComputeSandboxFlags(is_inside_mhtml, frame_sandbox_flags, policies);
policies.is_credentialless = is_credentialless;
return policies;
}
void NavigationPolicyContainerBuilder::ComputePolicies(
NavigationHandle* navigation_handle,
bool is_inside_mhtml,
network::mojom::WebSandboxFlags frame_sandbox_flags,
bool is_credentialless) {
DCHECK(!HasComputedPolicies());
ComputeIsWebSecureContext();
SetFinalPolicies(ComputeFinalPolicies(navigation_handle, is_inside_mhtml,
frame_sandbox_flags,
is_credentialless));
}
bool NavigationPolicyContainerBuilder::HasComputedPolicies() const {
return host_ != nullptr;
}
void NavigationPolicyContainerBuilder::SetAllowTopNavigationWithoutUserGesture(
bool allow_top) {
host_->SetCanNavigateTopWithoutUserGesture(allow_top);
}
void NavigationPolicyContainerBuilder::SetFinalPolicies(
PolicyContainerPolicies policies) {
DCHECK(!HasComputedPolicies());
host_ = base::MakeRefCounted<PolicyContainerHost>(std::move(policies));
}
const PolicyContainerPolicies& NavigationPolicyContainerBuilder::FinalPolicies()
const {
DCHECK(HasComputedPolicies());
return host_->policies();
}
blink::mojom::PolicyContainerPtr
NavigationPolicyContainerBuilder::CreatePolicyContainerForBlink() {
DCHECK(HasComputedPolicies());
return host_->CreatePolicyContainerForBlink();
}
scoped_refptr<PolicyContainerHost>
NavigationPolicyContainerBuilder::GetPolicyContainerHost() {
DCHECK(HasComputedPolicies());
CHECK(host_);
return host_;
}
scoped_refptr<PolicyContainerHost>
NavigationPolicyContainerBuilder::TakePolicyContainerHost() && {
DCHECK(HasComputedPolicies());
return std::move(host_);
}
void NavigationPolicyContainerBuilder::ResetForCrossDocumentRestart() {
host_ = nullptr;
delivered_policies_ = PolicyContainerPolicies();
}
} // namespace content