| // 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, |
| ¶ms->url, ¶ms->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(), ¤t_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 |