Overview

Medium
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactPrivilege elevation in WebUI
DescriptionPrivilege elevation in WebUI
ComponentWebUI
Bug ClassLogic 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.
Tracker497551905
Fix commitd074777b1332 (chromium/src) +15/-8
CWECWE-250
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-03-29 (per vendor advisory)
Fix landed2026-08-04 (public git)
Disclosed2026-09-08
Patch gap35 days — fix public in source before the advisory

Background

`chrome-untrusted://`
a WebUI scheme (kChromeUIUntrustedScheme) for browser-hosted UI that may process untrustworthy content, and which is meant to run with no WebUI bindings and no elevated URL-fetch privileges.
Requestable schemes
the per-WebUI list (requestable_schemes_, exposed via web_ui_->GetRequestableSchemes()) of URL schemes that the WebUI’s renderer process is allowed to fetch.
`ChildProcessSecurityPolicyImpl::GrantRequestScheme` / `CanRequestURL`
the browser-side security policy that records which schemes a renderer process may request and later answers whether that process is permitted to load a given URL.
Privilege elevation in WebUI
a lower-trust WebUI context gaining a capability reserved for higher-trust chrome:// UI, here the ability to request chrome:// and file:// URLs.

Root Cause Analysis

Every WebUIImpl was constructed with requestable_schemes_ pre-populated to {kChromeUIScheme, url::kFileScheme}, so the browser unconditionally treated chrome:// and file:// as requestable for any WebUI. When a frame’s WebUI type was set in content/browser/renderer_host/render_frame_host_impl.cc, the code iterated web_ui_->GetRequestableSchemes() and called ChildProcessSecurityPolicyImpl::GrantRequestScheme for each entry, thereby granting those schemes to the renderer process — including processes hosting chrome-untrusted:// content. The violated invariant is that a chrome-untrusted:// WebUI must not receive chrome:// or file:// request privileges.

The fix removes the default scheme initialization from the WebUIImpl constructor and, at the grant site, only calls web_ui_->AddRequestableScheme(kChromeUIScheme) and AddRequestableScheme(url::kFileScheme) when the frame’s GetSiteInstance()->GetSiteInfo().site_url() is not the kChromeUIUntrustedScheme, so untrusted WebUIs end up with an empty requestable-scheme set. The diff is purely a scope-tightening of who gets the grant; it adds a guard around an existing grant rather than changing any downstream URL-loading mechanism.

Key insight
The core mistake was granting chrome:// and file:// requestable schemes by default to all WebUIs regardless of trust level, which leaked those privileges to chrome-untrusted:// contexts; the fix conditions the grant on the site not being kChromeUIUntrustedScheme and drops the default from the constructor.

Attack Path

  1. Load an untrusted WebUI An attacker gets a chrome-untrusted:// document loaded (e.g. via a feature/UI that embeds untrusted WebUI content), which per the commit message retains this defect “throughout the lifetime,” not only at startup.
  2. Inherit the default grant On WebUI-type assignment the pre-patch render_frame_host_impl.cc loop reads the constructor-seeded requestable_schemes_ ({kChromeUIScheme, url::kFileScheme}) and calls ChildProcessSecurityPolicyImpl::GrantRequestScheme for the untrusted process, so CanRequestURL now returns true for chrome:// and file://.
  3. Request privileged URLs From the untrusted renderer the attacker requests file:// (e.g. file:///etc/passwd) and chrome:// (e.g. chrome://version) URLs that the untrusted context should never be able to fetch, as the new negative test assertions on CanRequestURL confirm.
  4. Escalate using the leaked reach Access to local files and internal chrome:// surfaces could be leveraged for information disclosure or further compromise (inferred); the diff itself only demonstrates the improper scheme grant, not any specific downstream read primitive or sandbox escape.

Impact Assessment

An attacker controlling a chrome-untrusted:// WebUI gains the ability to request chrome:// and file:// URLs from that renderer process, i.e. a privilege elevation within the WebUI/browser context beyond what the untrusted scheme is supposed to hold. The precondition is that untrusted WebUI content is loaded in a frame that undergoes WebUI-type assignment. The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.3. The advisory states only “Privilege elevation in WebUI” and reports no exploited-in-the-wild status or severity, and the patch demonstrates only the scheme grant.

Changed Functions

FunctionChangeNotes
IN_PROC_BROWSER_TEST_F
content/browser/webui/web_ui_security_browsertest.cc
modified

Files Changed

  • content/browser/webui/web_ui_impl.cc
  • content/browser/renderer_host/render_frame_host_impl.cc
  • content/browser/webui/web_ui_security_browsertest.cc

Audit Directions

  • Default-granted capabilities in constructors
    Look for security-relevant privilege sets (schemes, bindings, permissions) initialized to a permissive default in a base constructor and later applied without re-checking the actual trust level of the context.
  • Trust-level checks at grant sites
    Audit every ChildProcessSecurityPolicyImpl::Grant* / AddRequestableScheme call path to confirm it distinguishes chrome:// from chrome-untrusted:// (kChromeUIUntrustedScheme) rather than treating all WebUIs uniformly.
  • Lifetime vs. startup assumptions
    Re-examine WebUI privilege assignments that were reasoned about only for initial navigation, since, as the commit notes, the grant “persists throughout the lifetime” and can apply on later WebUI-type transitions.
From d074777b13325b2108b4c0c2565c7c239cafa526 Mon Sep 17 00:00:00 2001
From: Fred Shih <ffred@chromium.org>
Date: Tue, 04 Aug 2026 13:21:57 -0700
Subject: [PATCH] Fix sandbox escape for chrome-untrusted://

The bug only mentioned startup, but I guess this issue persists
throughout the lifetime. This change ensures that chrome-untrusted://
does not get access to chrome:// and file:// URLs by default.

I hope this doesn't break anything...

Bug: 497551905
Change-Id: Ibb48b22505a5603cb235f76e6ff56a23bd6d7e25
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8178737
Commit-Queue: Fred Shih <ffred@chromium.org>
Reviewed-by: Rakina Zata Amni <rakina@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1673628}
---
diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index 78ce44e..0ba81f0 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -13740,6 +13740,11 @@
   web_ui_type_ = new_web_ui_type;
 
   // WebUIs need the ability to request certain schemes.
+  if (!GetSiteInstance()->GetSiteInfo().site_url().SchemeIs(
+          kChromeUIUntrustedScheme)) {
+    web_ui_->AddRequestableScheme(kChromeUIScheme);
+    web_ui_->AddRequestableScheme(url::kFileScheme);
+  }
   for (const auto& scheme : web_ui_->GetRequestableSchemes()) {
     ChildProcessSecurityPolicyImpl::GetInstance()->GrantRequestScheme(
         GetProcess()->GetDeprecatedID(), scheme);
diff --git a/content/browser/webui/web_ui_impl.cc b/content/browser/webui/web_ui_impl.cc
index aa277dde..0fd5d93 100644
--- a/content/browser/webui/web_ui_impl.cc
+++ b/content/browser/webui/web_ui_impl.cc
@@ -215,8 +215,7 @@
 }
 
 WebUIImpl::WebUIImpl(WebContents* web_contents)
-    : requestable_schemes_({kChromeUIScheme, url::kFileScheme}),
-      web_contents_(web_contents),
+    : web_contents_(web_contents),
       web_contents_observer_(
           std::make_unique<WebUIMainFrameObserver>(this, web_contents_)) {
   DCHECK(web_contents_);
diff --git a/content/browser/webui/web_ui_security_browsertest.cc b/content/browser/webui/web_ui_security_browsertest.cc
index 87a9e46..f4ba4f57 100644
--- a/content/browser/webui/web_ui_security_browsertest.cc
+++ b/content/browser/webui/web_ui_security_browsertest.cc
@@ -58,7 +58,8 @@
   ScopedWebUIControllerFactoryRegistration factory_registration_{&factory_};
 };
 
-// Verify chrome-untrusted:// have no bindings.
+// Verify chrome-untrusted:// have no bindings and cannot request chrome or file
+// URLs.
 IN_PROC_BROWSER_TEST_F(WebUISecurityTest, UntrustedNoBindings) {
   auto* web_contents = shell()->web_contents();
   WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig(
@@ -67,17 +68,19 @@
   const GURL untrusted_url(GetChromeUntrustedUIURL("test-host/title1.html"));
   EXPECT_TRUE(NavigateToURL(web_contents, untrusted_url));
 
+  auto process_id =
+      *shell()->web_contents()->GetPrimaryMainFrame()->GetProcess()->GetID();
   EXPECT_FALSE(ChildProcessSecurityPolicyImpl::GetInstance()->HasWebUIBindings(
-      shell()
-          ->web_contents()
-          ->GetPrimaryMainFrame()
-          ->GetProcess()
-          ->GetDeprecatedID()));
+      process_id));
   EXPECT_TRUE(shell()
                   ->web_contents()
                   ->GetPrimaryMainFrame()
                   ->GetEnabledBindings()
                   .empty());
+  EXPECT_FALSE(ChildProcessSecurityPolicyImpl::GetInstance()->CanRequestURL(
+      process_id, GURL("file:///etc/passwd")));
+  EXPECT_FALSE(ChildProcessSecurityPolicyImpl::GetInstance()->CanRequestURL(
+      process_id, GURL("chrome://version")));
 }
 
 // Loads a WebUI which does not have any bindings.
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/webui/web_ui_security_browsertest.cc b/content/browser/webui/web_ui_security_browsertest.cc
index 87a9e46..f4ba4f57 100644
--- a/content/browser/webui/web_ui_security_browsertest.cc
+++ b/content/browser/webui/web_ui_security_browsertest.cc
@@ -58,7 +58,8 @@
   ScopedWebUIControllerFactoryRegistration factory_registration_{&factory_};
 };
 
-// Verify chrome-untrusted:// have no bindings.
+// Verify chrome-untrusted:// have no bindings and cannot request chrome or file
+// URLs.
 IN_PROC_BROWSER_TEST_F(WebUISecurityTest, UntrustedNoBindings) {
   auto* web_contents = shell()->web_contents();
   WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig(
@@ -67,17 +68,19 @@
   const GURL untrusted_url(GetChromeUntrustedUIURL("test-host/title1.html"));
   EXPECT_TRUE(NavigateToURL(web_contents, untrusted_url));
 
+  auto process_id =
+      *shell()->web_contents()->GetPrimaryMainFrame()->GetProcess()->GetID();
   EXPECT_FALSE(ChildProcessSecurityPolicyImpl::GetInstance()->HasWebUIBindings(
-      shell()
-          ->web_contents()
-          ->GetPrimaryMainFrame()
-          ->GetProcess()
-          ->GetDeprecatedID()));
+      process_id));
   EXPECT_TRUE(shell()
                   ->web_contents()
                   ->GetPrimaryMainFrame()
                   ->GetEnabledBindings()
                   .empty());
+  EXPECT_FALSE(ChildProcessSecurityPolicyImpl::GetInstance()->CanRequestURL(
+      process_id, GURL("file:///etc/passwd")));
+  EXPECT_FALSE(ChildProcessSecurityPolicyImpl::GetInstance()->CanRequestURL(
+      process_id, GURL("chrome://version")));
 }
 
 // Loads a WebUI which does not have any bindings.
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.