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 <[email protected]> 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 <[email protected]> Commit-Queue: Nasko Oskov <[email protected]> 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