Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in WebUI
DescriptionIncorrect authorization in WebUI
ComponentWebUI
Sub-componentWebui
Bug ClassAuthorization Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker537466493
Fix commit140c7324157c (chromium/src) +56/-6
CWECWE-863
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-07-21 (per vendor advisory)
Fix landed2026-08-03 (public git)
Disclosed2026-09-08
Patch gap36 days — fix public in source before the advisory

Background

`is_renderer_initiated`
A per-navigation flag on NavigationParams that records whether a navigation was triggered by web content (renderer) versus by trusted browser code, and which gates many downstream authorization checks that browser-initiated navigations are allowed to skip.
WebUI trust policy
WebUIController::TrustPolicy distinguishes kTrusted privileged pages served over chrome:// from less-trusted pages served over chrome-untrusted://, exposed via web_ui->GetController()->GetTrustPolicy().
`chrome-untrusted://`
A WebUI scheme used to host code that processes untrustworthy or web-derived input; it still has a WebUI object but is deliberately not granted the trust of ordinary chrome:// pages.
`Referrer()`
An empty referrer that WebUI navigations substitute so that chrome://-style URLs (which may carry search terms or other sensitive data) are never leaked to destination sites.

Root Cause Analysis

In content/browser/renderer_host/navigator.cc, two navigation paths tested only whether the source frame had a WebUI object (render_frame_host->web_ui()), and if so unconditionally forced the navigation to be treated as browser-initiated by setting is_renderer_initiated = false. Because a chrome-untrusted:// frame also has a WebUI object, a renderer-initiated navigation started from untrusted (web-facing) WebUI content was mislabeled as browser-initiated, violating the invariant that navigations originating from untrustworthy content must remain renderer-initiated so their authorization checks apply. The is_renderer_initiated flag is precisely what many browser-side security checks rely on to decide which restrictions to enforce, and browser-initiated navigations are permitted to bypass a number of those restrictions.

The fix wraps the is_renderer_initiated = false assignment in a guard that only demotes the flag when web_ui->GetController()->GetTrustPolicy() == WebUIController::TrustPolicy::kTrusted, so only genuinely trusted chrome:// pages get browser-initiated treatment. Untrusted WebUI navigations now retain their default renderer-initiated status (is_renderer_initiated stays true), keeping the downstream authorization checks in force. This is a tightening: an over-broad “any WebUI” condition is narrowed to “trusted WebUI only.”

Key insight
The single mistake was equating “has a WebUI object” with “is trusted,” which let chrome-untrusted:// frames inherit the browser-initiated exemption meant only for privileged chrome:// pages; the fix distinguishes them by consulting GetTrustPolicy() and demotes to browser-initiated exclusively for kTrusted controllers.

Attack Path

  1. Reach untrusted WebUI An attacker gets code running in a chrome-untrusted:// frame, e.g. by supplying content or input that a chrome-untrusted:// surface renders or processes.
  2. Trigger a renderer-initiated navigation From that frame, script issues an ordinary navigation such as location.href = <url>, which is renderer-initiated by nature.
  3. Flag is misclassified In navigator.cc, the pre-patch code sees a non-null web_ui() and sets is_renderer_initiated = false, laundering the untrusted navigation into a browser-initiated one.
  4. Bypass authorization checks Because is_renderer_initiated is now false, downstream checks that gate renderer-initiated navigations are skipped, so the untrusted page performs a navigation it should not have been authorized for (inferred — the specific check bypassed is not shown in the diff).

Impact Assessment

An attacker with execution in a chrome-untrusted:// WebUI frame gains the ability to have its renderer-initiated navigations treated as trusted browser-initiated navigations, bypassing authorization checks that key off is_renderer_initiated in the browser process. The precondition is control of content in a chrome-untrusted:// surface. The advisory classifies this as incorrect authorization in WebUI; no exploited-in-the-wild status is stated, and any escalation beyond the mislabeling itself is (inferred).

Changed Functions

FunctionChangeNotes
AdditionalSchemesWebUINavigationBrowserTest
content/browser/webui/web_ui_navigation_browsertest.cc
modified
AdditionalSchemesWebUINavigationBrowserTest
content/browser/webui/web_ui_navigation_browsertest.cc
modified

Files Changed

  • content/browser/renderer_host/navigator.cc
  • content/browser/webui/web_ui_navigation_browsertest.cc

Audit Directions

  • Presence-of-WebUI as trust proxy
    Search for other sites that branch on render_frame_host->web_ui() (or any non-null WebUI check) to grant a privilege or skip a check, and confirm they instead consult GetTrustPolicy() to exclude chrome-untrusted://.
  • `is_renderer_initiated` demotion points
    Audit every place that sets is_renderer_initiated/params.is_renderer_initiated to false, verifying the source is actually trusted browser code or a kTrusted WebUI and not merely any WebUI or embedder-supplied frame.
  • chrome-untrusted:// capability parity
    Review other WebUI-gated behaviors (referrer suppression, binding grants, URL-load allowances) for the same conflation of trusted and untrusted WebUI schemes, since chrome-untrusted:// frames carry a WebUI object but must not inherit chrome:// trust.
From 140c7324157ca9e303d196e3f96568abf741962b Mon Sep 17 00:00:00 2001
From: Giovanni Ortuño Urquidi <[email protected]>
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 <[email protected]>
Reviewed-by: Alex Moshchuk <[email protected]>
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 [email protected]

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.

View on issue tracker