blob: 9c4baf61a1e8a50c8ff523651f35ec471911448a [file]
// Copyright 2019 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/ipc_utils.h"
#include <optional>
#include <utility>
#include "base/debug/crash_logging.h"
#include "base/debug/dump_without_crashing.h"
#include "base/strings/string_util.h"
#include "base/strings/to_string.h"
#include "content/browser/bad_message.h"
#include "content/browser/blob_storage/chrome_blob_storage_context.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/security/cpsp/child_process_security_policy_impl.h"
#include "content/common/features.h"
#include "content/common/frame.mojom.h"
#include "content/common/navigation_params_utils.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/child_process_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/common/url_constants.h"
#include "mojo/public/cpp/system/message_pipe.h"
#include "net/http/http_request_headers.h"
#include "third_party/blink/public/mojom/navigation/navigation_params.mojom.h"
#include "ui/base/window_open_disposition.h"
namespace content {
namespace {
// Validates that the specified `disposition` could be legitimately sent by the
// renderer, as defined by NavigationPolicyToDisposition() in
// render_frame_impl.cc.
bool IsValidRendererDisposition(WindowOpenDisposition disposition) {
switch (disposition) {
case WindowOpenDisposition::CURRENT_TAB:
case WindowOpenDisposition::NEW_FOREGROUND_TAB:
case WindowOpenDisposition::NEW_BACKGROUND_TAB:
case WindowOpenDisposition::NEW_POPUP:
case WindowOpenDisposition::NEW_WINDOW:
case WindowOpenDisposition::SAVE_TO_DISK:
case WindowOpenDisposition::NEW_PICTURE_IN_PICTURE:
case WindowOpenDisposition::NEW_SPLIT_VIEW:
return true;
default:
// Certain dispositions, such as SWITCH_TO_TAB, are only used internally
// within the browser process and should not be triggerable by a renderer
// process. Allowing a compromised renderer to send those could let it
// manipulate other tabs in unintended ways. See
// https://crbug.com/486761170.
return false;
}
}
// Validates that |received_token| is non-null iff associated with a blob: URL.
bool VerifyBlobToken(
ChildProcessId process_id,
const mojo::PendingRemote<blink::mojom::BlobURLToken>& received_token,
const GURL& received_url) {
CHECK(process_id, base::NotFatalUntil::M154);
if (received_token.is_valid()) {
if (!received_url.SchemeIsBlob()) {
bad_message::ReceivedBadMessage(
process_id, bad_message::BLOB_URL_TOKEN_FOR_NON_BLOB_URL);
return false;
}
}
return true;
}
bool VerifyInitiatorOrigin(
ChildProcessId process_id,
const url::Origin& initiator_origin,
const RenderFrameHostImpl* current_rfh = nullptr,
GURL* navigation_url = nullptr,
std::optional<blink::LocalFrameToken>* initiator_frame_token = nullptr) {
// Important Note about opaque origins: these checks used to be skipped for
// opaque origins in two tricky cases, error pages and MHTML subframes. These
// exemptions are no longer needed now that ChildProcessSecurityPolicy's
// enforcements have been switched to use committed origin tracking. Any error
// page or MHTML subframe that could legitimately initiate a navigation has
// already committed in this process, so its (opaque) origin has been recorded
// by ChildProcessSecurityPolicyImpl::AddCommittedOrigin and the HostsOrigin()
// check below will accept it, even if the precursor doesn't match the process
// lock. This is covered in tests such as ErrorPageNavigationReload,
// ErrorPageNavigationReload_InSubframe_BlockedByClient, and
// NavigationMhtmlBrowserTest.DataIframe.
//
// Warning: avoid skipping this check for future cases, as doing so carries
// security consequences, allowing the renderer to claim an opaque initiator
// with an arbitrary precursor. See crbug.com/516398679 and
// crbug.com/517606780.
auto* policy = ChildProcessSecurityPolicyImpl::GetInstance();
// TODO(crbug.com/379869738): Remove GetUnsafeValue.
if (!policy->HostsOrigin(process_id.GetUnsafeValue(), initiator_origin)) {
if (navigation_url) {
static auto* const navigation_url_key =
base::debug::AllocateCrashKeyString(
"navigation_url", base::debug::CrashKeySize::Size256);
base::debug::SetCrashKeyString(
navigation_url_key,
navigation_url->DeprecatedGetOriginAsURL().spec());
}
if (initiator_frame_token && initiator_frame_token->has_value()) {
if (RenderFrameHostImpl* initiator_render_frame_host =
RenderFrameHostImpl::FromFrameToken(
process_id, initiator_frame_token->value())) {
static auto* const initiator_rfh_origin_key =
base::debug::AllocateCrashKeyString(
"initiator_rfh_origin", base::debug::CrashKeySize::Size256);
base::debug::SetCrashKeyString(
initiator_rfh_origin_key,
initiator_render_frame_host->GetLastCommittedOrigin()
.GetDebugString());
}
}
if (current_rfh) {
auto bool_to_crash_key = [](bool b) { return base::ToString(b); };
static auto* const is_main_frame_key =
base::debug::AllocateCrashKeyString(
"is_main_frame", base::debug::CrashKeySize::Size32);
base::debug::SetCrashKeyString(
is_main_frame_key, bool_to_crash_key(current_rfh->is_main_frame()));
static auto* const is_outermost_frame_key =
base::debug::AllocateCrashKeyString(
"is_outermost_frame", base::debug::CrashKeySize::Size32);
base::debug::SetCrashKeyString(
is_outermost_frame_key,
bool_to_crash_key(current_rfh->IsOutermostMainFrame()));
static auto* const is_on_initial_empty_document_key =
base::debug::AllocateCrashKeyString(
"is_on_initial_empty_doc", base::debug::CrashKeySize::Size32);
base::debug::SetCrashKeyString(
is_on_initial_empty_document_key,
bool_to_crash_key(
current_rfh->frame_tree_node()->is_on_initial_empty_document()));
static auto* const last_committed_origin_key =
base::debug::AllocateCrashKeyString(
"last_committed_origin", base::debug::CrashKeySize::Size256);
base::debug::SetCrashKeyString(
last_committed_origin_key,
current_rfh->GetLastCommittedOrigin().GetDebugString());
if (current_rfh->GetParentOrOuterDocumentOrEmbedder()) {
static auto* const parent_etc_origin_key =
base::debug::AllocateCrashKeyString(
"parent_etc_origin", base::debug::CrashKeySize::Size256);
base::debug::SetCrashKeyString(
parent_etc_origin_key,
current_rfh->GetParentOrOuterDocumentOrEmbedder()
->GetLastCommittedOrigin()
.GetDebugString());
}
if (FrameTreeNode* opener = current_rfh->frame_tree_node()->opener()) {
static auto* const opener_origin_key =
base::debug::AllocateCrashKeyString(
"opener_origin", base::debug::CrashKeySize::Size256);
base::debug::SetCrashKeyString(opener_origin_key,
opener->current_frame_host()
->GetLastCommittedOrigin()
.GetDebugString());
}
}
bad_message::ReceivedBadMessage(process_id,
bad_message::INVALID_INITIATOR_ORIGIN);
return false;
}
return true;
}
} // namespace
bool VerifyDownloadUrlParams(RenderProcessHost* process,
const blink::mojom::DownloadURLParams& params) {
CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M154);
CHECK(process);
ChildProcessId process_id = process->GetID();
// Verifies |params.blob_url_token| is appropriately set.
if (!VerifyBlobToken(process_id, params.blob_url_token, params.url))
return false;
// Verify |params.initiator_origin|.
if (params.initiator_origin &&
!VerifyInitiatorOrigin(process_id, *params.initiator_origin))
return false;
// Verify |params.referrer|.
if (params.referrer && !params.referrer->url.is_empty()) {
auto* policy = ChildProcessSecurityPolicyImpl::GetInstance();
if (!policy->HostsOrigin(process_id.GetUnsafeValue(),
url::Origin::Create(params.referrer->url))) {
bad_message::ReceivedBadMessage(
process_id, bad_message::RFH_DOWNLOAD_URL_INVALID_REFERRER);
return false;
}
}
// If |params.url| is not set, this must be a large data URL being passed
// through |params.data_url_blob|.
if (!params.url.is_valid() && !params.data_url_blob.is_valid())
return false;
// Verification succeeded.
return true;
}
bool VerifyOpenURLParams(RenderFrameHostImpl* current_rfh,
RenderProcessHost* process,
const blink::mojom::OpenURLParamsPtr& params,
GURL* out_validated_url,
scoped_refptr<network::SharedURLLoaderFactory>*
out_blob_url_loader_factory) {
CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M154);
CHECK(current_rfh, base::NotFatalUntil::M154);
CHECK(process, base::NotFatalUntil::M154);
CHECK(out_validated_url, base::NotFatalUntil::M154);
CHECK(out_blob_url_loader_factory, base::NotFatalUntil::M154);
ChildProcessId process_id = process->GetID();
// Verify |params.url| and populate |out_validated_url|.
*out_validated_url = params->url;
process->FilterURL(false, out_validated_url);
// Verify |params.blob_url_token| and populate |out_blob_url_loader_factory|.
if (!VerifyBlobToken(process_id, params->blob_url_token, params->url))
return false;
if (params->blob_url_token.is_valid()) {
*out_blob_url_loader_factory =
ChromeBlobStorageContext::URLLoaderFactoryForToken(
process->GetStoragePartition(), std::move(params->blob_url_token));
}
// Verify |params.post_body|.
auto* policy = ChildProcessSecurityPolicyImpl::GetInstance();
if (!policy->CanReadRequestBody(process, params->post_body)) {
bad_message::ReceivedBadMessage(process,
bad_message::ILLEGAL_UPLOAD_PARAMS);
return false;
}
// Verify |params.initiator_origin|.
if (!VerifyInitiatorOrigin(process_id, params->initiator_origin, current_rfh,
&params->url, &params->initiator_frame_token)) {
return false;
}
if (!VerifyNavigationHeaders(process, params->extra_headers)) {
return false;
}
if (params->initiator_base_url) {
// `initiator_base_url` should only be defined for about:blank and
// about:srcdoc navigations, and should never be an empty GURL (if it is not
// nullopt).
if (params->initiator_base_url->is_empty() ||
!(out_validated_url->IsAboutBlank() ||
out_validated_url->IsAboutSrcdoc())) {
return false;
}
}
// Verify that the initiator frame can navigate `current_rfh`.
if (!VerifyNavigationInitiator(current_rfh, params->initiator_frame_token,
process_id)) {
return false;
}
if (params->is_container_initiated) {
if (!current_rfh->GetParent() ||
(current_rfh->GetParent()->GetFrameToken() !=
params->initiator_frame_token)) {
mojo::ReportBadMessage(
"container initiated navigation from non-parent process");
return false;
}
}
// Certain dispositions should never be sent from the renderer, so terminate
// the renderer process if an unexpected disposition is encountered.
if (!IsValidRendererDisposition(params->disposition)) {
bad_message::ReceivedBadMessage(
process, bad_message::RFH_OPEN_URL_INVALID_DISPOSITION);
return false;
}
// Verification succeeded.
return true;
}
bool VerifyBeginNavigationCommonParams(
const RenderFrameHostImpl& current_rfh,
blink::mojom::CommonNavigationParams* common_params,
std::optional<blink::LocalFrameToken>& initiator_frame_token) {
CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M154);
CHECK(common_params, base::NotFatalUntil::M154);
RenderProcessHost* process = current_rfh.GetProcess();
ChildProcessId process_id = process->GetID();
// Verify (and possibly rewrite) |url|.
process->FilterURL(false, &common_params->url);
if (common_params->url.SchemeIs(kChromeErrorScheme)) {
mojo::ReportBadMessage("Renderer cannot request error page URLs directly");
return false;
}
// Verify |post_data|.
auto* policy = ChildProcessSecurityPolicyImpl::GetInstance();
if (!policy->CanReadRequestBody(process, common_params->post_data)) {
bad_message::ReceivedBadMessage(process,
bad_message::ILLEGAL_UPLOAD_PARAMS);
return false;
}
// Verify |transition| is webby.
if (!PageTransitionIsWebTriggerable(
ui::PageTransitionFromInt(common_params->transition))) {
bad_message::ReceivedBadMessage(
process, bad_message::RFHI_BEGIN_NAVIGATION_NON_WEBBY_TRANSITION);
return false;
}
// Verify |initiator_origin|.
if (!common_params->initiator_origin.has_value()) {
bad_message::ReceivedBadMessage(
process, bad_message::RFHI_BEGIN_NAVIGATION_MISSING_INITIATOR_ORIGIN);
return false;
}
if (!VerifyInitiatorOrigin(
process_id, common_params->initiator_origin.value(), &current_rfh,
&common_params->url, &initiator_frame_token)) {
return false;
}
// Verify |base_url_for_data_url|.
if (!common_params->base_url_for_data_url.is_empty()) {
// Kills the process. http://crbug.com/726142
bad_message::ReceivedBadMessage(
process, bad_message::RFH_BASE_URL_FOR_DATA_URL_SPECIFIED);
return false;
}
// Verify |initiator_base_url|. The value is allowed to be nullopt, but if it
// isn't then it's required to be non-empty (the renderer is supposed to
// guarantee this). If this condition isn't met, CHECK in NavigationRequest's
// constructor will fail.
if (common_params->initiator_base_url &&
common_params->initiator_base_url->is_empty()) {
bad_message::ReceivedBadMessage(
process, bad_message::RFH_INITIATOR_BASE_URL_IS_EMPTY);
return false;
}
// Asynchronous (browser-controlled, but) renderer-initiated navigations can
// not be same-document. Allowing this incorrectly could have us try to
// navigate an existing document to a different site.
if (NavigationTypeUtils::IsSameDocument(common_params->navigation_type))
return false;
// Verification succeeded.
return true;
}
bool VerifyClientSideRedirectUrl(const RenderFrameHostImpl& current_rfh,
GURL* client_side_redirect_url) {
CHECK_CURRENTLY_ON(BrowserThread::UI);
CHECK(client_side_redirect_url);
// `client_side_redirect_url` is only populated if the navigation's transition
// type is a client side redirect. For all other renderer-initiated
// navigations, it is intentionally empty.
if (client_side_redirect_url->is_empty()) {
return true;
}
RenderProcessHost* process = current_rfh.GetProcess();
CHECK(process);
process->FilterURL(false, client_side_redirect_url);
// Verify that `process` has hosted `redirect_origin` either as a standard
// tuple origin or as the precursor of an opaque origin (e.g. when the
// redirect is initiated by a sandboxed document). URLs blocked by FilterURL()
// are rewritten to about:blank#blocked, which is treated as the
// `current_rfh`'s origin.
url::Origin redirect_origin = url::Origin::Resolve(
*client_side_redirect_url, current_rfh.GetLastCommittedOrigin());
auto* policy = ChildProcessSecurityPolicyImpl::GetInstance();
ChildProcessId process_id = process->GetID();
if (!policy->HostsOrigin(process_id.GetUnsafeValue(), redirect_origin) &&
!policy->HostsOrigin(process_id.GetUnsafeValue(),
redirect_origin.DeriveNewOpaqueOrigin())) {
bad_message::ReceivedBadMessage(
process, bad_message::RFHI_INVALID_CLIENT_SIDE_REDIRECT_URL);
return false;
}
return true;
}
bool VerifyCreateNewWindowParams(const RenderFrameHostImpl& current_rfh,
const mojom::CreateNewWindowParams& params) {
CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M154);
RenderProcessHost* process = current_rfh.GetProcess();
// Certain dispositions should never be sent from the renderer, so terminate
// the renderer process if an unexpected disposition is encountered.
if (!IsValidRendererDisposition(params.disposition)) {
bad_message::ReceivedBadMessage(
process, bad_message::RFH_CREATE_NEW_WINDOW_INVALID_DISPOSITION);
return false;
}
if (params.pip_options &&
params.disposition != WindowOpenDisposition::NEW_PICTURE_IN_PICTURE) {
bad_message::ReceivedBadMessage(
process, bad_message::RFH_CREATE_NEW_WINDOW_INVALID_PIP_OPTIONS);
return false;
}
// Verify `form_submission_post_data`.
auto* policy = ChildProcessSecurityPolicyImpl::GetInstance();
if (!policy->CanReadRequestBody(process, params.form_submission_post_data)) {
bad_message::ReceivedBadMessage(process,
bad_message::ILLEGAL_UPLOAD_PARAMS);
return false;
}
return true;
}
bool VerifyNavigationInitiator(
RenderFrameHostImpl* current_rfh,
const std::optional<blink::LocalFrameToken>& initiator_frame_token,
ChildProcessId initiator_process_id) {
// Verify that a frame inside a fenced frame cannot navigate its ancestors,
// unless the frame being navigated is the outermost main frame.
if (current_rfh->IsOutermostMainFrame())
return true;
if (!initiator_frame_token)
return true;
RenderFrameHostImpl* initiator_render_frame_host =
RenderFrameHostImpl::FromFrameToken(initiator_process_id,
initiator_frame_token.value());
if (!initiator_render_frame_host)
return true;
// Verify that a frame cannot navigate a frame with a different fenced frame
// nonce, unless the navigating frame is a fenced frame root and its owner
// frame has the same fenced frame nonce as the initiator frame (e.g. in a
// A(A1,A2(FF)) setup, A, A1, and A2 are all allowed to navigate FF).
std::optional<base::UnguessableToken> initiator_fenced_frame_nonce =
initiator_render_frame_host->frame_tree_node()->GetFencedFrameNonce();
if (initiator_fenced_frame_nonce !=
current_rfh->frame_tree_node()->GetFencedFrameNonce()) {
if (!current_rfh->IsFencedFrameRoot() ||
current_rfh->frame_tree_node()
->GetParentOrOuterDocument()
->frame_tree_node()
->GetFencedFrameNonce() != initiator_fenced_frame_nonce) {
mojo::ReportBadMessage(
"The fenced frame nonces of initiator and current frame don't match, "
"nor is the current frame a fenced frame root whose owner frame has "
"the same fenced frame nonce as the initiator frame.");
return false;
}
}
if (!initiator_render_frame_host->IsNestedWithinFencedFrame())
return true;
FrameTreeNode* node = initiator_render_frame_host->frame_tree_node();
if (node == current_rfh->frame_tree_node())
return true;
while (node) {
node = node->parent() ? node->parent()->frame_tree_node() : nullptr;
if (node == current_rfh->frame_tree_node()) {
mojo::ReportBadMessage(
"A frame in a fenced frame tree cannot navigate an ancestor frame.");
return false;
}
}
return true;
}
bool VerifyNavigationHeaders(RenderProcessHost* process,
const std::string& headers) {
// Navigation headers may be LF-separated and are normalized to CRLF
// before being applied to the outgoing request.
// AddHeadersFromString() splits only on CRLF, so apply the same normalization
// here to ensure consistent header verification.
std::string headers_crlf;
base::ReplaceChars(headers, "\n", "\r\n", &headers_crlf);
net::HttpRequestHeaders parsed_headers;
parsed_headers.AddHeadersFromString(headers_crlf);
for (net::HttpRequestHeaders::Iterator header(parsed_headers);
header.GetNext();) {
// Headers should be strictly allowlisted because there can be security
// consequences if a compromised renderer can set arbitrary headers (e.g.,
// for CSRF prevention).
//
// This list allowlists `Origin`, but the value of the `Origin` header is
// further validated in NavigationRequest::AddAdditionalRequestHeaders.
if (header.name() != net::HttpRequestHeaders::kUpgradeInsecureRequests &&
header.name() != net::HttpRequestHeaders::kOrigin &&
header.name() != net::HttpRequestHeaders::kContentType &&
header.name() != net::HttpRequestHeaders::kUserAgent &&
header.name() != net::HttpRequestHeaders::kSecPurpose &&
header.name() != net::HttpRequestHeaders::kDNT &&
header.name() != net::HttpRequestHeaders::kSecGPC) {
if (base::FeatureList::IsEnabled(
features::kKillOnInvalidNavigationHeaders)) {
SCOPED_CRASH_KEY_STRING64("Bug487795397", "invalid_header",
header.name());
bad_message::ReceivedBadMessage(
process, bad_message::RFH_INVALID_NAVIGATION_HEADERS);
return false;
}
}
}
return true;
}
} // namespace content