Chrome · Navigation
CVE-2026-113006
OOB in Navigation
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/renderer_host/navigator.cc |
modified |
Files Changed
content/browser/renderer_host/navigator.cccontent/browser/security_exploit_browsertest.cccontent/browser/webui/web_ui_security_browsertest.cc
Patch
From 7815ce630360134b1c0e60389ec49d4642cbc15c Mon Sep 17 00:00:00 2001
From: Nasko Oskov <nasko@chromium.org>
Date: Mon, 28 Sep 2026 17:05:58 -0700
Subject: [PATCH] Enforce WebUI trust attribution from initiator in NavigateFromFrameProxy
When a frame navigates another frame via a remote frame proxy,
NavigateFromFrameProxy checks whether the navigation originates from a
trusted WebUI to determine if the navigation should be upgraded to
browser-initiated and whether the referrer should be stripped.
Previously, NavigateFromFrameProxy checked `render_frame_host->web_ui()`,
which represents the target frame being navigated. If an untrusted
renderer navigated a remote frame proxy targeting a WebUI page, the
navigation was improperly treated as browser-initiated, and its referrer
was stripped.
This CL fixes the trust check to look up the initiator RenderFrameHost
via RenderFrameHostImpl::FromFrameToken(initiator_process_id,
*initiator_frame_token) and check its web_ui() instead, matching the
pattern in Navigator::RequestOpenURL.
If the initiator frame token is absent or belongs to a frame that was
already destroyed, FromFrameToken returns nullptr, safely falling back
to treating the navigation as renderer-initiated.
TAG=agy
CONV=8fea1fd0-fbfb-482a-a2fa-aac1c74bc30e
Bug: 496650333
Change-Id: I78ab2da5deba9684d04800cc74e1287c12f01f1f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8429459
Reviewed-by: Alex Moshchuk <alexmos@chromium.org>
Commit-Queue: Nasko Oskov <nasko@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1706519}
---
diff --git a/content/browser/renderer_host/navigator.cc b/content/browser/renderer_host/navigator.cc
index 4da4a8d4..ee261a5 100644
--- a/content/browser/renderer_host/navigator.cc
+++ b/content/browser/renderer_host/navigator.cc
@@ -1214,22 +1214,26 @@
return;
}
- // TODO(creis): Determine if this transfer started as a browser-initiated
- // navigation. See https://crbug.com/495161.
+ RenderFrameHostImpl* initiator_rfh =
+ initiator_frame_token ? RenderFrameHostImpl::FromFrameToken(
+ initiator_process_id, *initiator_frame_token)
+ : nullptr;
bool is_renderer_initiated = true;
Referrer referrer_to_use(referrer);
- if (WebUI* web_ui = render_frame_host->web_ui()) {
- // Note that we hide the referrer for Web UI pages. We don't really want
- // web sites to see a referrer of "chrome://blah" (and some chrome: URLs
- // might have search terms or other stuff we don't want to send to the
- // site), so we send no referrer.
- referrer_to_use = Referrer();
+ if (initiator_rfh) {
+ if (WebUI* web_ui = initiator_rfh->web_ui()) {
+ // Note that we hide the referrer for Web UI pages. We don't really want
+ // web sites to see a referrer of "chrome://blah" (and some chrome: URLs
+ // might have search terms or other stuff we don't want to send to the
+ // site), so we send no referrer.
+ referrer_to_use = Referrer();
- // Navigations in trusted Web UI pages count as browser-initiated
- // navigations.
- if (web_ui->GetController()->GetTrustPolicy() ==
- WebUIController::TrustPolicy::kTrusted) {
- is_renderer_initiated = false;
+ // Navigations in trusted Web UI pages count as browser-initiated
+ // navigations.
+ if (web_ui->GetController()->GetTrustPolicy() ==
+ WebUIController::TrustPolicy::kTrusted) {
+ is_renderer_initiated = false;
+ }
}
}
diff --git a/content/browser/security_exploit_browsertest.cc b/content/browser/security_exploit_browsertest.cc
index 0ba83bb1..06d2553b 100644
--- a/content/browser/security_exploit_browsertest.cc
+++ b/content/browser/security_exploit_browsertest.cc
@@ -78,12 +78,14 @@
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/navigation_handle_observer.h"
#include "content/public/test/render_frame_host_test_support.h"
+#include "content/public/test/scoped_web_ui_controller_factory_registration.h"
#include "content/public/test/test_content_browser_client.h"
#include "content/public/test/test_frame_navigation_observer.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/test_utils.h"
#include "content/public/test/url_loader_interceptor.h"
+#include "content/public/test/web_ui_browsertest_util.h"
#include "content/shell/browser/shell.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "content/test/did_commit_navigation_interceptor.h"
@@ -145,6 +147,7 @@
#include "third_party/blink/public/mojom/page/widget.mojom.h"
#include "third_party/blink/public/mojom/permissions/permission.mojom.h"
#include "third_party/blink/public/mojom/widget/platform_widget.mojom.h"
+#include "ui/webui/untrusted_web_ui_browsertest_util.h"
using ::testing::HasSubstr;
using ::testing::Optional;
@@ -6585,5 +6588,274 @@
"Navigation.InitialDocument.NonEmptyCompositorFrameTreatedAsAccess", true,
1);
}
+// Test that when an unprivileged cross-process iframe embedded in a WebUI page
+// navigates its parent WebUI frame via RemoteFrameHost::OpenURL, the resulting
+// navigation is NOT upgraded to browser-initiated based on the target WebUI
+// frame. Regression test for crbug.com/496650333.
+IN_PROC_BROWSER_TEST_F(
+ SecurityExploitBrowserTest,
+ OpenURLFromRemoteFrame_WebUITarget_UnprivilegedInitiatorDoesNotUpgrade) {
+ TestWebUIControllerFactory factory;
+ ScopedWebUIControllerFactoryRegistration registration(&factory);
+
+ if (IsIsolatedOriginRequiredToGuaranteeDedicatedProcess()) {
+ IsolateOriginsForTesting(embedded_test_server(), shell()->web_contents(),
+ {"b.com"});
+ }
+
+ GURL start_url = GetWebUIURL(
+ "test-webui/page_with_blank_iframe.html?noxfo=true¬rustedtypes=true"
+ "&childsrc=child-src *;");
+ EXPECT_TRUE(NavigateToURL(shell(), start_url));
+
+ FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
+ ->GetPrimaryFrameTree()
+ .root();
+ EXPECT_TRUE(root->current_frame_host()->web_ui());
+ EXPECT_EQ(1u, root->child_count());
+
+ FrameTreeNode* child = root->child_at(0);
+ GURL child_url = embedded_test_server()->GetURL("b.com", "/title1.html");
+ NavigateFrameToURL(child, child_url);
+
+ SiteInstanceImpl* child_instance =
+ child->current_frame_host()->GetSiteInstance();
+ EXPECT_NE(root->current_frame_host()->GetProcess(),
+ child->current_frame_host()->GetProcess());
+
+ // The proxy in the child frame's process representing the WebUI root frame.
+ RenderFrameProxyHost* proxy =
+ root->current_frame_host()
+ ->browsing_context_state()
+ ->GetRenderFrameProxyHost(child_instance->group());
+ ASSERT_TRUE(proxy);
+
+ GURL dest_url = embedded_test_server()->GetURL("b.com", "/title2.html");
+ TestNavigationManager nav_manager(shell()->web_contents(), dest_url);
+
+ auto params = CreateOpenURLParams(dest_url, child->current_frame_host());
+ params->initiator_frame_token = child->current_frame_host()->GetFrameToken();
+ params->referrer = blink::mojom::Referrer::New(
+ child_url, network::mojom::ReferrerPolicy::kDefault);
+
+ static_cast<blink::mojom::RemoteFrameHost*>(proxy)->OpenURL(
+ std::move(params));
+
+ EXPECT_TRUE(nav_manager.WaitForRequestStart());
+
+ NavigationHandle* handle = nav_manager.GetNavigationHandle();
+ ASSERT_TRUE(handle);
+ EXPECT_TRUE(handle->IsRendererInitiated());
+ EXPECT_EQ(child_url, handle->GetReferrer().url);
+}
+
+// Test that when an untrusted WebUI iframe (chrome-untrusted://) embedded in a
+// WebUI page navigates its parent WebUI frame via RemoteFrameHost::OpenURL,
+// the navigation is NOT upgraded to browser-initiated because its trust
+// policy is kUntrusted.
+IN_PROC_BROWSER_TEST_F(
+ SecurityExploitBrowserTest,
+ OpenURLFromRemoteFrame_WebUITarget_UntrustedInitiatorDoesNotUpgrade) {
+ TestWebUIControllerFactory factory;
+ ScopedWebUIControllerFactoryRegistration registration(&factory);
+
+ TestUntrustedDataSourceHeaders headers;
+ headers.no_xfo = true;
+ headers.no_trusted_types = true;
+ ScopedWebUIConfigRegistration untrusted_registration(
+ std::make_unique<ui::TestUntrustedWebUIConfig>("test-untrusted-webui",
+ headers));
+
+ GURL start_url = GetWebUIURL(
+ "test-webui/page_with_blank_iframe.html?noxfo=true¬rustedtypes=true"
+ "&childsrc=child-src * chrome-untrusted:;");
+ EXPECT_TRUE(NavigateToURL(shell(), start_url));
+
+ FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
+ ->GetPrimaryFrameTree()
+ .root();
+ EXPECT_TRUE(root->current_frame_host()->web_ui());
+ EXPECT_EQ(1u, root->child_count());
+
+ FrameTreeNode* child = root->child_at(0);
+ GURL child_url = GetChromeUntrustedUIURL("test-untrusted-webui/title1.html");
+ NavigateFrameToURL(child, child_url);
+
+ SiteInstanceImpl* child_instance =
+ child->current_frame_host()->GetSiteInstance();
+ EXPECT_NE(root->current_frame_host()->GetProcess(),
+ child->current_frame_host()->GetProcess());
+ ASSERT_TRUE(child->current_frame_host()->web_ui());
+ EXPECT_EQ(
+ WebUIController::TrustPolicy::kUntrusted,
+ child->current_frame_host()->web_ui()->GetController()->GetTrustPolicy());
+
+ // The proxy in the child frame's process representing the WebUI root frame.
+ RenderFrameProxyHost* proxy =
+ root->current_frame_host()
+ ->browsing_context_state()
+ ->GetRenderFrameProxyHost(child_instance->group());
+ ASSERT_TRUE(proxy);
+
+ GURL dest_url = embedded_test_server()->GetURL("b.com", "/title2.html");
+ TestNavigationManager nav_manager(shell()->web_contents(), dest_url);
+
+ auto params = CreateOpenURLParams(dest_url, child->current_frame_host());
+ params->initiator_frame_token = child->current_frame_host()->GetFrameToken();
+ params->referrer = blink::mojom::Referrer::New(
+ child_url, network::mojom::ReferrerPolicy::kDefault);
+
+ static_cast<blink::mojom::RemoteFrameHost*>(proxy)->OpenURL(
+ std::move(params));
+
+ EXPECT_TRUE(nav_manager.WaitForRequestStart());
+
+ NavigationHandle* handle = nav_manager.GetNavigationHandle();
+ ASSERT_TRUE(handle);
+ EXPECT_TRUE(handle->IsRendererInitiated());
+ EXPECT_TRUE(handle->GetReferrer().url.is_empty());
+}
+
+// Test that if an initiating subframe is deleted/detached before the browser
+// processes OpenURL targeting a WebUI frame (so FromFrameToken returns
+// nullptr), the navigation safely fails closed and remains renderer-initiated.
+IN_PROC_BROWSER_TEST_F(
+ SecurityExploitBrowserTest,
+ OpenURLFromRemoteFrame_WebUITarget_DeletedInitiatorDoesNotUpgrade) {
+ TestWebUIControllerFactory factory;
+ ScopedWebUIControllerFactoryRegistration registration(&factory);
+
+ if (IsIsolatedOriginRequiredToGuaranteeDedicatedProcess()) {
+ IsolateOriginsForTesting(embedded_test_server(), shell()->web_contents(),
+ {"b.com"});
+ }
+
+ // Load a WebUI page with a b.com iframe.
+ GURL start_url = GetWebUIURL(
+ "test-webui/page_with_blank_iframe.html?noxfo=true¬rustedtypes=true"
+ "&childsrc=child-src *;");
+ EXPECT_TRUE(NavigateToURL(shell(), start_url));
+
+ FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
+ ->GetPrimaryFrameTree()
+ .root();
+ EXPECT_TRUE(root->current_frame_host()->web_ui());
+ EXPECT_EQ(1u, root->child_count());
+
+ FrameTreeNode* child = root->child_at(0);
+ GURL child_url = embedded_test_server()->GetURL("b.com", "/title1.html");
+ NavigateFrameToURL(child, child_url);
+
+ // Add a grandchild iframe inside child.
+ EXPECT_TRUE(ExecJs(child->current_frame_host(),
+ "const f = document.createElement('iframe');"
+ "f.src = '/title1.html';"
+ "document.body.appendChild(f);"));
+ EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
+ EXPECT_EQ(1u, child->child_count());
+ FrameTreeNode* grandchild = child->child_at(0);
+
+ SiteInstanceImpl* child_instance =
+ child->current_frame_host()->GetSiteInstance();
+ // Capture the grandchild's frame token before it is deleted.
+ blink::LocalFrameToken deleted_frame_token =
+ grandchild->current_frame_host()->GetFrameToken();
+
+ // The proxy in the b.com process representing the WebUI root frame.
+ RenderFrameProxyHost* proxy =
+ root->current_frame_host()
+ ->browsing_context_state()
+ ->GetRenderFrameProxyHost(child_instance->group());
+ ASSERT_TRUE(proxy);
+
+ // Delete the grandchild iframe from the DOM so its RenderFrameHost is
+ // destroyed.
+ EXPECT_TRUE(ExecJs(child->current_frame_host(),
+ "document.querySelector('iframe').remove();"));
+ EXPECT_EQ(0u, child->child_count());
+
+ // Verify that FromFrameToken no longer finds the destroyed initiator frame.
+ EXPECT_FALSE(RenderFrameHostImpl::FromFrameToken(
+ child_instance->GetProcess()->GetID(), deleted_frame_token));
+
+ // Create OpenURL params using the live child frame's document state, but
+ // specifying the deleted frame's token as the initiator_frame_token.
+ GURL dest_url = embedded_test_server()->GetURL("b.com", "/title2.html");
+ auto params = CreateOpenURLParams(dest_url, child->current_frame_host());
+ params->initiator_frame_token = deleted_frame_token;
+ params->referrer = blink::mojom::Referrer::New(
+ child_url, network::mojom::ReferrerPolicy::kDefault);
+
+ TestNavigationManager nav_manager(shell()->web_contents(), dest_url);
+
+ // Send OpenURL from the proxy with the deleted initiator frame's token.
+ static_cast<blink::mojom::RemoteFrameHost*>(proxy)->OpenURL(
+ std::move(params));
+
+ EXPECT_TRUE(nav_manager.WaitForRequestStart());
+
+ NavigationHandle* handle = nav_manager.GetNavigationHandle();
+ ASSERT_TRUE(handle);
+ EXPECT_TRUE(handle->IsRendererInitiated());
+ EXPECT_EQ(child_url, handle->GetReferrer().url);
+}
+
+// Test that if initiator_frame_token is nullopt when navigating a WebUI frame
+// via OpenURL, the navigation safely falls back to renderer-initiated.
+IN_PROC_BROWSER_TEST_F(
+ SecurityExploitBrowserTest,
+ OpenURLFromRemoteFrame_WebUITarget_NullInitiatorTokenDoesNotUpgrade) {
+ TestWebUIControllerFactory factory;
+ ScopedWebUIControllerFactoryRegistration registration(&factory);
+
+ if (IsIsolatedOriginRequiredToGuaranteeDedicatedProcess()) {
+ IsolateOriginsForTesting(embedded_test_server(), shell()->web_contents(),
+ {"b.com"});
+ }
+
+ GURL start_url = GetWebUIURL(
+ "test-webui/page_with_blank_iframe.html?noxfo=true¬rustedtypes=true"
+ "&childsrc=child-src *;");
+ EXPECT_TRUE(NavigateToURL(shell(), start_url));
+
+ FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
+ ->GetPrimaryFrameTree()
+ .root();
+ EXPECT_TRUE(root->current_frame_host()->web_ui());
+ EXPECT_EQ(1u, root->child_count());
+
+ FrameTreeNode* child = root->child_at(0);
+ GURL child_url = embedded_test_server()->GetURL("b.com", "/title1.html");
+ NavigateFrameToURL(child, child_url);
+
+ SiteInstanceImpl* child_instance =
+ child->current_frame_host()->GetSiteInstance();
+ EXPECT_NE(root->current_frame_host()->GetProcess(),
+ child->current_frame_host()->GetProcess());
+
+ RenderFrameProxyHost* proxy =
+ root->current_frame_host()
+ ->browsing_context_state()
+ ->GetRenderFrameProxyHost(child_instance->group());
+ ASSERT_TRUE(proxy);
+
+ GURL dest_url = embedded_test_server()->GetURL("b.com", "/title2.html");
+ TestNavigationManager nav_manager(shell()->web_contents(), dest_url);
+
+ auto params = CreateOpenURLParams(dest_url, child->current_frame_host());
+ params->initiator_frame_token = std::nullopt;
+ params->referrer = blink::mojom::Referrer::New(
+ child_url, network::mojom::ReferrerPolicy::kDefault);
+
+ static_cast<blink::mojom::RemoteFrameHost*>(proxy)->OpenURL(
+ std::move(params));
+
+ EXPECT_TRUE(nav_manager.WaitForRequestStart());
+
+ NavigationHandle* handle = nav_manager.GetNavigationHandle();
+ ASSERT_TRUE(handle);
+ EXPECT_TRUE(handle->IsRendererInitiated());
+ EXPECT_EQ(child_url, handle->GetReferrer().url);
+}
} // namespace content
diff --git a/content/browser/webui/web_ui_security_browsertest.cc b/content/browser/webui/web_ui_security_browsertest.cc
index f4ba4f57..2d3e133e 100644
--- a/content/browser/webui/web_ui_security_browsertest.cc
+++ b/content/browser/webui/web_ui_security_browsertest.cc
@@ -12,6 +12,8 @@
#include "base/threading/thread_restrictions.h"
#include "content/browser/process_lock.h"
#include "content/browser/renderer_host/frame_tree_node.h"
+#include "content/browser/renderer_host/render_frame_host_impl.h"
+#include "content/browser/renderer_host/render_frame_proxy_host.h"
#include "content/browser/security/cpsp/child_process_security_policy_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/browser/webui/web_ui_controller_factory_registry.h"
@@ -39,6 +41,7 @@
#include "content/shell/browser/shell.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "net/base/url_util.h"
+#include "third_party/blink/public/mojom/frame/frame.mojom.h"
#include "ui/webui/untrusted_web_ui_browsertest_util.h"
... (file diff truncated)
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/content/browser/security_exploit_browsertest.cc b/content/browser/security_exploit_browsertest.cc
index 0ba83bb1..06d2553b 100644
--- a/content/browser/security_exploit_browsertest.cc
+++ b/content/browser/security_exploit_browsertest.cc
@@ -78,12 +78,14 @@
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/navigation_handle_observer.h"
#include "content/public/test/render_frame_host_test_support.h"
+#include "content/public/test/scoped_web_ui_controller_factory_registration.h"
#include "content/public/test/test_content_browser_client.h"
#include "content/public/test/test_frame_navigation_observer.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/test_utils.h"
#include "content/public/test/url_loader_interceptor.h"
+#include "content/public/test/web_ui_browsertest_util.h"
#include "content/shell/browser/shell.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "content/test/did_commit_navigation_interceptor.h"
@@ -145,6 +147,7 @@
#include "third_party/blink/public/mojom/page/widget.mojom.h"
#include "third_party/blink/public/mojom/permissions/permission.mojom.h"
#include "third_party/blink/public/mojom/widget/platform_widget.mojom.h"
+#include "ui/webui/untrusted_web_ui_browsertest_util.h"
using ::testing::HasSubstr;
using ::testing::Optional;
@@ -6585,5 +6588,274 @@
"Navigation.InitialDocument.NonEmptyCompositorFrameTreatedAsAccess", true,
1);
}
+// Test that when an unprivileged cross-process iframe embedded in a WebUI page
+// navigates its parent WebUI frame via RemoteFrameHost::OpenURL, the resulting
+// navigation is NOT upgraded to browser-initiated based on the target WebUI
+// frame. Regression test for crbug.com/496650333.
+IN_PROC_BROWSER_TEST_F(
+ SecurityExploitBrowserTest,
+ OpenURLFromRemoteFrame_WebUITarget_UnprivilegedInitiatorDoesNotUpgrade) {
+ TestWebUIControllerFactory factory;
+ ScopedWebUIControllerFactoryRegistration registration(&factory);
+
+ if (IsIsolatedOriginRequiredToGuaranteeDedicatedProcess()) {
+ IsolateOriginsForTesting(embedded_test_server(), shell()->web_contents(),
+ {"b.com"});
+ }
+
+ GURL start_url = GetWebUIURL(
+ "test-webui/page_with_blank_iframe.html?noxfo=true¬rustedtypes=true"
+ "&childsrc=child-src *;");
+ EXPECT_TRUE(NavigateToURL(shell(), start_url));
+
+ FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
+ ->GetPrimaryFrameTree()
+ .root();
+ EXPECT_TRUE(root->current_frame_host()->web_ui());
+ EXPECT_EQ(1u, root->child_count());
+
+ FrameTreeNode* child = root->child_at(0);
+ GURL child_url = embedded_test_server()->GetURL("b.com", "/title1.html");
+ NavigateFrameToURL(child, child_url);
+
+ SiteInstanceImpl* child_instance =
+ child->current_frame_host()->GetSiteInstance();
+ EXPECT_NE(root->current_frame_host()->GetProcess(),
+ child->current_frame_host()->GetProcess());
+
+ // The proxy in the child frame's process representing the WebUI root frame.
+ RenderFrameProxyHost* proxy =
+ root->current_frame_host()
+ ->browsing_context_state()
+ ->GetRenderFrameProxyHost(child_instance->group());
+ ASSERT_TRUE(proxy);
+
+ GURL dest_url = embedded_test_server()->GetURL("b.com", "/title2.html");
+ TestNavigationManager nav_manager(shell()->web_contents(), dest_url);
+
+ auto params = CreateOpenURLParams(dest_url, child->current_frame_host());
+ params->initiator_frame_token = child->current_frame_host()->GetFrameToken();
+ params->referrer = blink::mojom::Referrer::New(
+ child_url, network::mojom::ReferrerPolicy::kDefault);
+
+ static_cast<blink::mojom::RemoteFrameHost*>(proxy)->OpenURL(
+ std::move(params));
+
+ EXPECT_TRUE(nav_manager.WaitForRequestStart());
+
+ NavigationHandle* handle = nav_manager.GetNavigationHandle();
+ ASSERT_TRUE(handle);
+ EXPECT_TRUE(handle->IsRendererInitiated());
+ EXPECT_EQ(child_url, handle->GetReferrer().url);
+}
+
+// Test that when an untrusted WebUI iframe (chrome-untrusted://) embedded in a
+// WebUI page navigates its parent WebUI frame via RemoteFrameHost::OpenURL,
+// the navigation is NOT upgraded to browser-initiated because its trust
+// policy is kUntrusted.
+IN_PROC_BROWSER_TEST_F(
+ SecurityExploitBrowserTest,
+ OpenURLFromRemoteFrame_WebUITarget_UntrustedInitiatorDoesNotUpgrade) {
+ TestWebUIControllerFactory factory;
+ ScopedWebUIControllerFactoryRegistration registration(&factory);
+
+ TestUntrustedDataSourceHeaders headers;
+ headers.no_xfo = true;
+ headers.no_trusted_types = true;
+ ScopedWebUIConfigRegistration untrusted_registration(
+ std::make_unique<ui::TestUntrustedWebUIConfig>("test-untrusted-webui",
+ headers));
+
+ GURL start_url = GetWebUIURL(
+ "test-webui/page_with_blank_iframe.html?noxfo=true¬rustedtypes=true"
+ "&childsrc=child-src * chrome-untrusted:;");
+ EXPECT_TRUE(NavigateToURL(shell(), start_url));
+
+ FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
+ ->GetPrimaryFrameTree()
+ .root();
+ EXPECT_TRUE(root->current_frame_host()->web_ui());
+ EXPECT_EQ(1u, root->child_count());
+
+ FrameTreeNode* child = root->child_at(0);
+ GURL child_url = GetChromeUntrustedUIURL("test-untrusted-webui/title1.html");
+ NavigateFrameToURL(child, child_url);
+
+ SiteInstanceImpl* child_instance =
+ child->current_frame_host()->GetSiteInstance();
+ EXPECT_NE(root->current_frame_host()->GetProcess(),
+ child->current_frame_host()->GetProcess());
+ ASSERT_TRUE(child->current_frame_host()->web_ui());
+ EXPECT_EQ(
+ WebUIController::TrustPolicy::kUntrusted,
+ child->current_frame_host()->web_ui()->GetController()->GetTrustPolicy());
+
+ // The proxy in the child frame's process representing the WebUI root frame.
+ RenderFrameProxyHost* proxy =
+ root->current_frame_host()
+ ->browsing_context_state()
+ ->GetRenderFrameProxyHost(child_instance->group());
+ ASSERT_TRUE(proxy);
+
+ GURL dest_url = embedded_test_server()->GetURL("b.com", "/title2.html");
+ TestNavigationManager nav_manager(shell()->web_contents(), dest_url);
+
+ auto params = CreateOpenURLParams(dest_url, child->current_frame_host());
+ params->initiator_frame_token = child->current_frame_host()->GetFrameToken();
+ params->referrer = blink::mojom::Referrer::New(
+ child_url, network::mojom::ReferrerPolicy::kDefault);
+
+ static_cast<blink::mojom::RemoteFrameHost*>(proxy)->OpenURL(
+ std::move(params));
+
+ EXPECT_TRUE(nav_manager.WaitForRequestStart());
+
+ NavigationHandle* handle = nav_manager.GetNavigationHandle();
+ ASSERT_TRUE(handle);
+ EXPECT_TRUE(handle->IsRendererInitiated());
+ EXPECT_TRUE(handle->GetReferrer().url.is_empty());
+}
+
+// Test that if an initiating subframe is deleted/detached before the browser
+// processes OpenURL targeting a WebUI frame (so FromFrameToken returns
+// nullptr), the navigation safely fails closed and remains renderer-initiated.
+IN_PROC_BROWSER_TEST_F(
+ SecurityExploitBrowserTest,
+ OpenURLFromRemoteFrame_WebUITarget_DeletedInitiatorDoesNotUpgrade) {
+ TestWebUIControllerFactory factory;
+ ScopedWebUIControllerFactoryRegistration registration(&factory);
+
+ if (IsIsolatedOriginRequiredToGuaranteeDedicatedProcess()) {
+ IsolateOriginsForTesting(embedded_test_server(), shell()->web_contents(),
+ {"b.com"});
+ }
+
+ // Load a WebUI page with a b.com iframe.
+ GURL start_url = GetWebUIURL(
+ "test-webui/page_with_blank_iframe.html?noxfo=true¬rustedtypes=true"
+ "&childsrc=child-src *;");
+ EXPECT_TRUE(NavigateToURL(shell(), start_url));
+
+ FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
+ ->GetPrimaryFrameTree()
+ .root();
+ EXPECT_TRUE(root->current_frame_host()->web_ui());
+ EXPECT_EQ(1u, root->child_count());
+
+ FrameTreeNode* child = root->child_at(0);
+ GURL child_url = embedded_test_server()->GetURL("b.com", "/title1.html");
+ NavigateFrameToURL(child, child_url);
+
+ // Add a grandchild iframe inside child.
+ EXPECT_TRUE(ExecJs(child->current_frame_host(),
+ "const f = document.createElement('iframe');"
+ "f.src = '/title1.html';"
+ "document.body.appendChild(f);"));
+ EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
+ EXPECT_EQ(1u, child->child_count());
+ FrameTreeNode* grandchild = child->child_at(0);
+
+ SiteInstanceImpl* child_instance =
+ child->current_frame_host()->GetSiteInstance();
+ // Capture the grandchild's frame token before it is deleted.
+ blink::LocalFrameToken deleted_frame_token =
+ grandchild->current_frame_host()->GetFrameToken();
+
+ // The proxy in the b.com process representing the WebUI root frame.
+ RenderFrameProxyHost* proxy =
+ root->current_frame_host()
+ ->browsing_context_state()
+ ->GetRenderFrameProxyHost(child_instance->group());
+ ASSERT_TRUE(proxy);
+
+ // Delete the grandchild iframe from the DOM so its RenderFrameHost is
+ // destroyed.
+ EXPECT_TRUE(ExecJs(child->current_frame_host(),
+ "document.querySelector('iframe').remove();"));
+ EXPECT_EQ(0u, child->child_count());
+
+ // Verify that FromFrameToken no longer finds the destroyed initiator frame.
+ EXPECT_FALSE(RenderFrameHostImpl::FromFrameToken(
+ child_instance->GetProcess()->GetID(), deleted_frame_token));
+
+ // Create OpenURL params using the live child frame's document state, but
+ // specifying the deleted frame's token as the initiator_frame_token.
+ GURL dest_url = embedded_test_server()->GetURL("b.com", "/title2.html");
+ auto params = CreateOpenURLParams(dest_url, child->current_frame_host());
+ params->initiator_frame_token = deleted_frame_token;
+ params->referrer = blink::mojom::Referrer::New(
+ child_url, network::mojom::ReferrerPolicy::kDefault);
+
+ TestNavigationManager nav_manager(shell()->web_contents(), dest_url);
+
+ // Send OpenURL from the proxy with the deleted initiator frame's token.
+ static_cast<blink::mojom::RemoteFrameHost*>(proxy)->OpenURL(
+ std::move(params));
+
+ EXPECT_TRUE(nav_manager.WaitForRequestStart());
+
+ NavigationHandle* handle = nav_manager.GetNavigationHandle();
+ ASSERT_TRUE(handle);
+ EXPECT_TRUE(handle->IsRendererInitiated());
+ EXPECT_EQ(child_url, handle->GetReferrer().url);
+}
+
+// Test that if initiator_frame_token is nullopt when navigating a WebUI frame
+// via OpenURL, the navigation safely falls back to renderer-initiated.
+IN_PROC_BROWSER_TEST_F(
+ SecurityExploitBrowserTest,
+ OpenURLFromRemoteFrame_WebUITarget_NullInitiatorTokenDoesNotUpgrade) {
+ TestWebUIControllerFactory factory;
+ ScopedWebUIControllerFactoryRegistration registration(&factory);
+
+ if (IsIsolatedOriginRequiredToGuaranteeDedicatedProcess()) {
+ IsolateOriginsForTesting(embedded_test_server(), shell()->web_contents(),
+ {"b.com"});
+ }
+
+ GURL start_url = GetWebUIURL(
+ "test-webui/page_with_blank_iframe.html?noxfo=true¬rustedtypes=true"
+ "&childsrc=child-src *;");
+ EXPECT_TRUE(NavigateToURL(shell(), start_url));
+
+ FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
+ ->GetPrimaryFrameTree()
+ .root();
+ EXPECT_TRUE(root->current_frame_host()->web_ui());
+ EXPECT_EQ(1u, root->child_count());
+
+ FrameTreeNode* child = root->child_at(0);
+ GURL child_url = embedded_test_server()->GetURL("b.com", "/title1.html");
+ NavigateFrameToURL(child, child_url);
+
+ SiteInstanceImpl* child_instance =
+ child->current_frame_host()->GetSiteInstance();
+ EXPECT_NE(root->current_frame_host()->GetProcess(),
+ child->current_frame_host()->GetProcess());
+
+ RenderFrameProxyHost* proxy =
+ root->current_frame_host()
+ ->browsing_context_state()
+ ->GetRenderFrameProxyHost(child_instance->group());
+ ASSERT_TRUE(proxy);
+
+ GURL dest_url = embedded_test_server()->GetURL("b.com", "/title2.html");
+ TestNavigationManager nav_manager(shell()->web_contents(), dest_url);
+
+ auto params = CreateOpenURLParams(dest_url, child->current_frame_host());
+ params->initiator_frame_token = std::nullopt;
+ params->referrer = blink::mojom::Referrer::New(
+ child_url, network::mojom::ReferrerPolicy::kDefault);
+
+ static_cast<blink::mojom::RemoteFrameHost*>(proxy)->OpenURL(
+ std::move(params));
+
+ EXPECT_TRUE(nav_manager.WaitForRequestStart());
+
+ NavigationHandle* handle = nav_manager.GetNavigationHandle();
+ ASSERT_TRUE(handle);
+ EXPECT_TRUE(handle->IsRendererInitiated());
+ EXPECT_EQ(child_url, handle->GetReferrer().url);
+}
... (truncated)
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page