Chrome · WebUI
CVE-2026-87584
Logic Error in WebUI
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
AdditionalSchemesWebUINavigationBrowserTestcontent/browser/webui/web_ui_navigation_browsertest.cc |
modified | |
AdditionalSchemesWebUINavigationBrowserTestcontent/browser/webui/web_ui_navigation_browsertest.cc |
modified |
Files Changed
content/browser/renderer_host/navigator.cccontent/browser/webui/web_ui_navigation_browsertest.cc
Patch
From 140c7324157ca9e303d196e3f96568abf741962b Mon Sep 17 00:00:00 2001
From: Giovanni Ortuño Urquidi <ortuno@chromium.org>
Date: Mon, 03 Aug 2026 13:07:25 -0700
Subject: [PATCH] webui: Mark untrusted webui navigations as renderer initiated
We use is_renderer_initiated for untrustworhty navigations, so we
shouldn't set it to false for chrome-untrusted://.
Bug: 537466493
Change-Id: Ie1b10be89066be6cc9186ba8261b7c3d7271c947
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8182929
Commit-Queue: Giovanni Ortuno Urquidi <ortuno@chromium.org>
Reviewed-by: Alex Moshchuk <alexmos@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1672853}
---
diff --git a/content/browser/renderer_host/navigator.cc b/content/browser/renderer_host/navigator.cc
index 425ad4aa..6375e2d1 100644
--- a/content/browser/renderer_host/navigator.cc
+++ b/content/browser/renderer_host/navigator.cc
@@ -48,6 +48,7 @@
#include "content/public/browser/page_navigator.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/restore_type.h"
+#include "content/public/browser/web_ui_controller.h"
#include "content/public/common/bindings_policy.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_constants.h"
@@ -1149,15 +1150,19 @@
params.source_render_process_id =
render_frame_host->GetProcess()->GetDeprecatedID();
- if (render_frame_host->web_ui()) {
+ 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.
params.referrer = Referrer();
- // Navigations in Web UI pages count as browser-initiated navigations.
- params.is_renderer_initiated = false;
+ // Navigations in trusted Web UI pages count as browser-initiated
+ // navigations.
+ if (web_ui->GetController()->GetTrustPolicy() ==
+ WebUIController::TrustPolicy::kTrusted) {
+ params.is_renderer_initiated = false;
+ }
}
params.blob_url_loader_factory = std::move(blob_url_loader_factory);
@@ -1212,15 +1217,19 @@
// navigation. See https://crbug.com/495161.
bool is_renderer_initiated = true;
Referrer referrer_to_use(referrer);
- if (render_frame_host->web_ui()) {
+ 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();
- // Navigations in Web UI pages count as browser-initiated navigations.
- 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;
+ }
}
if (is_renderer_initiated &&
diff --git a/content/browser/webui/web_ui_navigation_browsertest.cc b/content/browser/webui/web_ui_navigation_browsertest.cc
index 0748c319..f6e1f81 100644
--- a/content/browser/webui/web_ui_navigation_browsertest.cc
+++ b/content/browser/webui/web_ui_navigation_browsertest.cc
@@ -25,6 +25,7 @@
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_content_browser_client.h"
#include "content/public/test/content_browser_test_utils.h"
+#include "content/public/test/navigation_handle_observer.h"
#include "content/public/test/scoped_web_ui_controller_factory_registration.h"
#include "content/public/test/test_frame_navigation_observer.h"
#include "content/public/test/test_navigation_observer.h"
@@ -1225,6 +1226,46 @@
}
}
+// Verify that renderer-initiated navigations from chrome-untrusted:// frames
+// have is_renderer_initiated() == true.
+IN_PROC_BROWSER_TEST_F(WebUINavigationBrowserTest,
+ UntrustedWebUIsAreRendererInitiated) {
+ WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig(
+ std::make_unique<ui::TestUntrustedWebUIConfig>("test-host"));
+
+ GURL untrusted_url1(GetChromeUntrustedUIURL("test-host/title1.html"));
+ GURL untrusted_url2(GetChromeUntrustedUIURL("test-host/title2.html"));
+
+ EXPECT_TRUE(NavigateToURL(shell(), untrusted_url1));
+
+ NavigationHandleObserver untrusted_observer(shell()->web_contents(),
+ untrusted_url2);
+ TestNavigationObserver untrusted_nav_observer(shell()->web_contents(), 1);
+ EXPECT_TRUE(
+ ExecJs(shell(), JsReplace("location.href = $1;", untrusted_url2)));
+ untrusted_nav_observer.Wait();
+ EXPECT_TRUE(untrusted_observer.has_committed());
+ EXPECT_TRUE(untrusted_observer.is_renderer_initiated());
+}
+
+// Verify that renderer-initiated navigations from trusted chrome:// frames
+// count as browser-initiated (is_renderer_initiated() == false).
+IN_PROC_BROWSER_TEST_F(WebUINavigationBrowserTest,
+ TrustedWebUIsAreBrowserInitiated) {
+ GURL trusted_url1(GetWebUIURL("web-ui/title1.html"));
+ GURL trusted_url2(GetWebUIURL("web-ui/title2.html"));
+
+ EXPECT_TRUE(NavigateToURL(shell(), trusted_url1));
+
+ NavigationHandleObserver trusted_observer(shell()->web_contents(),
+ trusted_url2);
+ TestNavigationObserver trusted_nav_observer(shell()->web_contents(), 1);
+ EXPECT_TRUE(ExecJs(shell(), JsReplace("location.href = $1;", trusted_url2)));
+ trusted_nav_observer.Wait();
+ EXPECT_TRUE(trusted_observer.has_committed());
+ EXPECT_FALSE(trusted_observer.is_renderer_initiated());
+}
+
class AdditionalSchemesWebUINavigationBrowserTest : public ContentBrowserTest {
public:
AdditionalSchemesWebUINavigationBrowserTest() {
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/content/browser/webui/web_ui_navigation_browsertest.cc b/content/browser/webui/web_ui_navigation_browsertest.cc
index 0748c319..f6e1f81 100644
--- a/content/browser/webui/web_ui_navigation_browsertest.cc
+++ b/content/browser/webui/web_ui_navigation_browsertest.cc
@@ -25,6 +25,7 @@
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_content_browser_client.h"
#include "content/public/test/content_browser_test_utils.h"
+#include "content/public/test/navigation_handle_observer.h"
#include "content/public/test/scoped_web_ui_controller_factory_registration.h"
#include "content/public/test/test_frame_navigation_observer.h"
#include "content/public/test/test_navigation_observer.h"
@@ -1225,6 +1226,46 @@
}
}
+// Verify that renderer-initiated navigations from chrome-untrusted:// frames
+// have is_renderer_initiated() == true.
+IN_PROC_BROWSER_TEST_F(WebUINavigationBrowserTest,
+ UntrustedWebUIsAreRendererInitiated) {
+ WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig(
+ std::make_unique<ui::TestUntrustedWebUIConfig>("test-host"));
+
+ GURL untrusted_url1(GetChromeUntrustedUIURL("test-host/title1.html"));
+ GURL untrusted_url2(GetChromeUntrustedUIURL("test-host/title2.html"));
+
+ EXPECT_TRUE(NavigateToURL(shell(), untrusted_url1));
+
+ NavigationHandleObserver untrusted_observer(shell()->web_contents(),
+ untrusted_url2);
+ TestNavigationObserver untrusted_nav_observer(shell()->web_contents(), 1);
+ EXPECT_TRUE(
+ ExecJs(shell(), JsReplace("location.href = $1;", untrusted_url2)));
+ untrusted_nav_observer.Wait();
+ EXPECT_TRUE(untrusted_observer.has_committed());
+ EXPECT_TRUE(untrusted_observer.is_renderer_initiated());
+}
+
+// Verify that renderer-initiated navigations from trusted chrome:// frames
+// count as browser-initiated (is_renderer_initiated() == false).
+IN_PROC_BROWSER_TEST_F(WebUINavigationBrowserTest,
+ TrustedWebUIsAreBrowserInitiated) {
+ GURL trusted_url1(GetWebUIURL("web-ui/title1.html"));
+ GURL trusted_url2(GetWebUIURL("web-ui/title2.html"));
+
+ EXPECT_TRUE(NavigateToURL(shell(), trusted_url1));
+
+ NavigationHandleObserver trusted_observer(shell()->web_contents(),
+ trusted_url2);
+ TestNavigationObserver trusted_nav_observer(shell()->web_contents(), 1);
+ EXPECT_TRUE(ExecJs(shell(), JsReplace("location.href = $1;", trusted_url2)));
+ trusted_nav_observer.Wait();
+ EXPECT_TRUE(trusted_observer.has_committed());
+ EXPECT_FALSE(trusted_observer.is_renderer_initiated());
+}
+
class AdditionalSchemesWebUINavigationBrowserTest : public ContentBrowserTest {
public:
AdditionalSchemesWebUINavigationBrowserTest() {
Loading diff…
Original Bug Report
reported by al...@chromium.org
Stop treating navigations from chrome-untrusted:// as browser-initiated
Splitting this part of issue 497957278 out:
Navigator::RequestOpenURL contains a flawed check:
if (render_frame_host->web_ui()) {
// ...
// Navigations in Web UI pages count as browser-initiated navigations.
params.is_renderer_initiated = false;
}
Since the chrome-untrusted:// frame has a WebUI object, the code unconditionally sets params.is_renderer_initiated = false, turning potentially malicious renderer-initiated navigation requests into trusted browser-initiated requests.
AI-suggested fix:
Navigator::RequestOpenURL should not blindly set params.is_renderer_initiated = false for all WebUIs. It should ideally check WebUIController::GetTrustPolicy() and only launder navigations for trusted WebUIs.
References
On This Page