Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Chrome for iOS
DescriptionInsufficient validation of untrusted input in Chrome for iOS
ComponentChrome for iOS
Bug ClassLogic Error
Tracker508462149
Fix commit557fd2da3cad (chromium/src) +14/-21
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
TEST_F
ios/web/webui/crw_web_ui_scheme_handler_unittest.mm
modified

Files Changed

  • ios/web/webui/crw_web_ui_scheme_handler.mm
  • ios/web/webui/crw_web_ui_scheme_handler_unittest.mm
From 557fd2da3cad25b4ca1e5abb952deedc41e2ee0f Mon Sep 17 00:00:00 2001
From: Mike Dougherty <michaeldo@chromium.org>
Date: Mon, 18 May 2026 14:49:55 -0700
Subject: [PATCH] Ensure WebUI pages can only be opened when WKWebView URL exists

Additionally, update CRWWebUISchemeManagerTest tests to ensure the mock
WKWebView provides the URL value.
`CRWWebUISchemeManagerTest.StartTaskWithCorrectMainURL` was removed
because it was incorrectly calling into the scheme handler for a URL
which was not the expected custom scheme.

Fixed: 508462149
Change-Id: I49c5295f1b9a57d8a3857984620fb8d57426920b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7849183
Reviewed-by: Justin Cohen <justincohen@google.com>
Commit-Queue: Mike Dougherty <michaeldo@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1632417}
---

diff --git a/ios/web/webui/crw_web_ui_scheme_handler.mm b/ios/web/webui/crw_web_ui_scheme_handler.mm
index 6328a22..b9efad1 100644
--- a/ios/web/webui/crw_web_ui_scheme_handler.mm
+++ b/ios/web/webui/crw_web_ui_scheme_handler.mm
@@ -64,9 +64,10 @@
   // The "Access-Control-Allow-Origin" header is required below to allow
   // requests from any WebUI page to load chrome://resources URLs. However,
   // requests between different WebUI pages are blocked directly instead.
-  if (!URL.DomainIs(web::kWebUIResourcesHost) && webView.URL &&
-      url::SchemeHostPort(URL) !=
-          url::SchemeHostPort(net::GURLWithNSURL(webView.URL))) {
+  if (!webView.URL ||
+      (!URL.DomainIs(web::kWebUIResourcesHost) &&
+       url::SchemeHostPort(URL) !=
+           url::SchemeHostPort(net::GURLWithNSURL(webView.URL)))) {
     NSError* error = [NSError
         errorWithDomain:NSURLErrorDomain
                    code:NSURLErrorNoPermissionsToReadFile
diff --git a/ios/web/webui/crw_web_ui_scheme_handler_unittest.mm b/ios/web/webui/crw_web_ui_scheme_handler_unittest.mm
index 72a6c77..ad39a59 100644
--- a/ios/web/webui/crw_web_ui_scheme_handler_unittest.mm
+++ b/ios/web/webui/crw_web_ui_scheme_handler_unittest.mm
@@ -145,6 +145,8 @@
   NSMutableURLRequest* request =
       [NSMutableURLRequest requestWithURL:GetWebUIURL()];
   request.mainDocumentURL = GetWebUIURL();
+  OCMStub([web_view URL]).andReturn(request.mainDocumentURL);
+
   url_scheme_task.request = request;
 
   [scheme_handler webView:web_view startURLSchemeTask:url_scheme_task];
@@ -166,6 +168,7 @@
       [NSMutableURLRequest requestWithURL:components_url.URL];
   request.mainDocumentURL = request.URL;
   url_scheme_task.request = request;
+  OCMStub([web_view URL]).andReturn(request.URL);
 
   [scheme_handler webView:web_view startURLSchemeTask:url_scheme_task];
 
@@ -176,6 +179,7 @@
 
   // Check with a different URL.
   request.mainDocumentURL = [NSURL URLWithString:@"invalidScheme://page"];
+  OCMStub([web_view URL]).andReturn(request.mainDocumentURL);
   url_scheme_task.request = request;
   [scheme_handler webView:web_view startURLSchemeTask:url_scheme_task];
 
@@ -185,24 +189,6 @@
   EXPECT_EQ(NSURLErrorUnsupportedURL, url_scheme_task.error.code);
 }
 
-// Tests that calling start on the scheme handler returns some data when the URL
-// is *not* a WebUI URL but the main document URL is.
-TEST_F(CRWWebUISchemeManagerTest, StartTaskWithCorrectMainURL) {
-  CRWWebUISchemeHandler* scheme_handler = CreateSchemeHandler();
-  id web_view = OCMClassMock([WKWebView class]);
-  FakeSchemeTask* url_scheme_task = [[FakeSchemeTask alloc] init];
-  NSMutableURLRequest* request = [NSMutableURLRequest
-      requestWithURL:[NSURL URLWithString:@"https://notAWebUIURL"]];
-  request.mainDocumentURL = GetWebUIURL();
-  url_scheme_task.request = request;
-
-  [scheme_handler webView:web_view startURLSchemeTask:url_scheme_task];
-
-  RespondWithData(net::GURLWithNSURL(request.URL), "{}");
-  EXPECT_TRUE(url_scheme_task.receivedData);
-  EXPECT_FALSE(url_scheme_task.receivedError);
-}
-
 // Tests that calling start on the scheme handler returns an error when the URL
 // is correct but the mainDocumentURL is wrong.
 TEST_F(CRWWebUISchemeManagerTest, StartTaskWithWrongMainDocumentURL) {
@@ -212,6 +198,7 @@
   NSMutableURLRequest* request =
       [NSMutableURLRequest requestWithURL:GetWebUIURL()];
   request.mainDocumentURL = [NSURL URLWithString:@"https://notAWebUIURL"];
+  OCMStub([web_view URL]).andReturn(request.mainDocumentURL);
   url_scheme_task.request = request;
 
   [scheme_handler webView:web_view startURLSchemeTask:url_scheme_task];
@@ -230,6 +217,7 @@
   NSMutableURLRequest* request =
       [NSMutableURLRequest requestWithURL:GetWebUIURL()];
   request.mainDocumentURL = GetWebUIURL();
+  OCMStub([web_view URL]).andReturn(request.mainDocumentURL);
   url_scheme_task.request = request;
 
   [scheme_handler webView:web_view startURLSchemeTask:url_scheme_task];
@@ -250,6 +238,7 @@
   NSMutableURLRequest* request = [NSMutableURLRequest
       requestWithURL:[NSURL URLWithString:@"chrome://clown/res/clown.js"]];
   request.mainDocumentURL = [NSURL URLWithString:@"chrome://clown/"];
+  OCMStub([web_view URL]).andReturn(request.mainDocumentURL);
   url_scheme_task.request = request;
   [scheme_handler webView:web_view startURLSchemeTask:url_scheme_task];
   RespondWithData(net::GURLWithNSURL(request.URL), "{}");
@@ -262,6 +251,7 @@
   request = [NSMutableURLRequest
       requestWithURL:[NSURL URLWithString:@"chrome://clown/res/clown.css"]];
   request.mainDocumentURL = [NSURL URLWithString:@"chrome://clown/"];
+  OCMStub([web_view URL]).andReturn(request.mainDocumentURL);
   url_scheme_task.request = request;
   [scheme_handler webView:web_view startURLSchemeTask:url_scheme_task];
   RespondWithData(net::GURLWithNSURL(request.URL), "{}");
@@ -274,6 +264,7 @@
   request = [NSMutableURLRequest
       requestWithURL:[NSURL URLWithString:@"chrome://clown/res/clown.svg"]];
   request.mainDocumentURL = [NSURL URLWithString:@"chrome://clown/"];
+  OCMStub([web_view URL]).andReturn(request.mainDocumentURL);
   url_scheme_task.request = request;
   [scheme_handler webView:web_view startURLSchemeTask:url_scheme_task];
   RespondWithData(net::GURLWithNSURL(request.URL), "{}");
@@ -287,6 +278,7 @@
       requestWithURL:[NSURL
                          URLWithString:@"chrome://clown/res/clown.anything"]];
   request.mainDocumentURL = [NSURL URLWithString:@"chrome://clown/"];
+  OCMStub([web_view URL]).andReturn(request.mainDocumentURL);
   url_scheme_task.request = request;
   [scheme_handler webView:web_view startURLSchemeTask:url_scheme_task];
   RespondWithData(net::GURLWithNSURL(request.URL), "{}");
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/ios/web/webui/crw_web_ui_scheme_handler_unittest.mm b/ios/web/webui/crw_web_ui_scheme_handler_unittest.mm
index 72a6c77..ad39a59 100644
--- a/ios/web/webui/crw_web_ui_scheme_handler_unittest.mm
+++ b/ios/web/webui/crw_web_ui_scheme_handler_unittest.mm
@@ -145,6 +145,8 @@
   NSMutableURLRequest* request =
       [NSMutableURLRequest requestWithURL:GetWebUIURL()];
   request.mainDocumentURL = GetWebUIURL();
+  OCMStub([web_view URL]).andReturn(request.mainDocumentURL);
+
   url_scheme_task.request = request;
 
   [scheme_handler webView:web_view startURLSchemeTask:url_scheme_task];
@@ -166,6 +168,7 @@
       [NSMutableURLRequest requestWithURL:components_url.URL];
   request.mainDocumentURL = request.URL;
   url_scheme_task.request = request;
+  OCMStub([web_view URL]).andReturn(request.URL);
 
   [scheme_handler webView:web_view startURLSchemeTask:url_scheme_task];
 
@@ -176,6 +179,7 @@
 
   // Check with a different URL.
   request.mainDocumentURL = [NSURL URLWithString:@"invalidScheme://page"];
+  OCMStub([web_view URL]).andReturn(request.mainDocumentURL);
   url_scheme_task.request = request;
   [scheme_handler webView:web_view startURLSchemeTask:url_scheme_task];
 
@@ -185,24 +189,6 @@
   EXPECT_EQ(NSURLErrorUnsupportedURL, url_scheme_task.error.code);
 }
 
-// Tests that calling start on the scheme handler returns some data when the URL
-// is *not* a WebUI URL but the main document URL is.
-TEST_F(CRWWebUISchemeManagerTest, StartTaskWithCorrectMainURL) {
-  CRWWebUISchemeHandler* scheme_handler = CreateSchemeHandler();
-  id web_view = OCMClassMock([WKWebView class]);
-  FakeSchemeTask* url_scheme_task = [[FakeSchemeTask alloc] init];
-  NSMutableURLRequest* request = [NSMutableURLRequest
-      requestWithURL:[NSURL URLWithString:@"https://notAWebUIURL"]];
-  request.mainDocumentURL = GetWebUIURL();
-  url_scheme_task.request = request;
-
-  [scheme_handler webView:web_view startURLSchemeTask:url_scheme_task];
-
-  RespondWithData(net::GURLWithNSURL(request.URL), "{}");
-  EXPECT_TRUE(url_scheme_task.receivedData);
-  EXPECT_FALSE(url_scheme_task.receivedError);
-}
-
 // Tests that calling start on the scheme handler returns an error when the URL
 // is correct but the mainDocumentURL is wrong.
 TEST_F(CRWWebUISchemeManagerTest, StartTaskWithWrongMainDocumentURL) {
@@ -212,6 +198,7 @@
   NSMutableURLRequest* request =
       [NSMutableURLRequest requestWithURL:GetWebUIURL()];
   request.mainDocumentURL = [NSURL URLWithString:@"https://notAWebUIURL"];
+  OCMStub([web_view URL]).andReturn(request.mainDocumentURL);
   url_scheme_task.request = request;
 
   [scheme_handler webView:web_view startURLSchemeTask:url_scheme_task];
@@ -230,6 +217,7 @@
   NSMutableURLRequest* request =
       [NSMutableURLRequest requestWithURL:GetWebUIURL()];
   request.mainDocumentURL = GetWebUIURL();
+  OCMStub([web_view URL]).andReturn(request.mainDocumentURL);
   url_scheme_task.request = request;
 
   [scheme_handler webView:web_view startURLSchemeTask:url_scheme_task];
@@ -250,6 +238,7 @@
   NSMutableURLRequest* request = [NSMutableURLRequest
       requestWithURL:[NSURL URLWithString:@"chrome://clown/res/clown.js"]];
   request.mainDocumentURL = [NSURL URLWithString:@"chrome://clown/"];
+  OCMStub([web_view URL]).andReturn(request.mainDocumentURL);
   url_scheme_task.request = request;
   [scheme_handler webView:web_view startURLSchemeTask:url_scheme_task];
   RespondWithData(net::GURLWithNSURL(request.URL), "{}");
@@ -262,6 +251,7 @@
   request = [NSMutableURLRequest
       requestWithURL:[NSURL URLWithString:@"chrome://clown/res/clown.css"]];
   request.mainDocumentURL = [NSURL URLWithString:@"chrome://clown/"];
+  OCMStub([web_view URL]).andReturn(request.mainDocumentURL);
   url_scheme_task.request = request;
   [scheme_handler webView:web_view startURLSchemeTask:url_scheme_task];
   RespondWithData(net::GURLWithNSURL(request.URL), "{}");
@@ -274,6 +264,7 @@
   request = [NSMutableURLRequest
       requestWithURL:[NSURL URLWithString:@"chrome://clown/res/clown.svg"]];
   request.mainDocumentURL = [NSURL URLWithString:@"chrome://clown/"];
+  OCMStub([web_view URL]).andReturn(request.mainDocumentURL);
   url_scheme_task.request = request;
   [scheme_handler webView:web_view startURLSchemeTask:url_scheme_task];
   RespondWithData(net::GURLWithNSURL(request.URL), "{}");
@@ -287,6 +278,7 @@
       requestWithURL:[NSURL
                          URLWithString:@"chrome://clown/res/clown.anything"]];
   request.mainDocumentURL = [NSURL URLWithString:@"chrome://clown/"];
+  OCMStub([web_view URL]).andReturn(request.mainDocumentURL);
   url_scheme_task.request = request;
   [scheme_handler webView:web_view startURLSchemeTask:url_scheme_task];
   RespondWithData(net::GURLWithNSURL(request.URL), "{}");
Loading diff…

Original Bug Report

reported by li...@chromium.org

Confused Deputy in CRWWebUISchemeHandler allows Sandbox Escape via spoofed mainDocumentURL

Flapjack, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A logic flaw in CRWWebUISchemeHandler allows a compromised renderer process on iOS to bypass cross-origin checks and read sensitive internal browser data. By spoofing the mainDocumentURL of a request in a newly created window where webView.URL is nil, the attacker can bypass validation and access arbitrary chrome:// resources.

Affected files:

  • ios/web/webui/crw_web_ui_scheme_handler.mm
  • ios/web/web_state/ui/wk_web_view_configuration_provider.mm

Estimated timestamp from git blame: 2019-03-18

Description

In iOS Chromium, CRWWebUISchemeHandler manages requests to chrome:// URLs. To determine if a request is authorized, the handler performs two checks:

  1. WebUI Validation: It verifies that the mainDocumentURL of the request belongs to a recognized WebUI origin (e.g., chrome://sync-internals).
  2. Cross-Origin Check: To prevent one WebUI from accessing another’s data, it compares the requested URL’s origin with the origin of the current webView.URL.

The vulnerability stems from how the cross-origin check is implemented:

// ios/web/webui/crw_web_ui_scheme_handler.mm
if (!URL.DomainIs(web::kWebUIResourcesHost) && webView.URL &&
    url::SchemeHostPort(URL) !=
        url::SchemeHostPort(net::GURLWithNSURL(webView.URL))) {
  // Block request
}

In Objective-C, nil is falsy. If webView.URL is nil, the entire if condition short-circuits to false, and the access-denied block is skipped entirely.

When a new WKWebView is created (such as when window.open() is called), its URL property is initially nil and remains so until a navigation is fully committed.

Furthermore, the mainDocumentURL property is derived from the NSURLRequest provided by the WKURLSchemeTask, which originates from the (potentially compromised) renderer process via IPC. Since iOS lacks the ChildProcessSecurityPolicy process-level isolation present on desktop platforms, the browser implicitly trusts the renderer’s mainDocumentURL for the first validation check.

Potential Attack Path

  1. An attacker achieves arbitrary code execution within the WebKit WebContent (renderer) process.
  2. The attacker executes window.open() to create a new WKWebView in the browser process. At this moment, the WKWebView’s URL property is nil.
  3. Before any navigation commits in the new window, the attacker’s renderer initiates a request (e.g., via fetch) for a sensitive resource like chrome://prefs-internals.
  4. The attacker spoofs the IPC payload, setting the request’s mainDocumentURL to a valid WebUI origin like chrome://sync-internals.
  5. The browser process receives the request in CRWWebUISchemeHandler.
  6. The first check passes because chrome://sync-internals is a valid WebUI.
  7. The cross-origin check evaluates webView.URL. Since it is nil, the check short-circuits, completely bypassing the security restriction.
  8. URLFetcherBlockAdapter executes the request using the browser’s trusted SharedURLLoaderFactory and returns the sensitive JSON data directly to the compromised renderer.

Security Impact

Sandbox Escape / Privilege Escalation. A compromised renderer process can bypass process-level security boundaries to read highly sensitive internal browser data across all WebUI components, such as user preferences, emails, and internal device identifiers found in chrome://prefs-internals.

Suggested Fix

The cross-origin check must not fail open when webView.URL is nil. If the webView.URL is nil, the web view has no committed origin and should not be permitted to make cross-origin requests to sensitive chrome:// resources. The logic should be updated to deny access if the origins mismatch or if the webView.URL origin cannot be established for a non-resource request.

Evaluated with Chrome root at commit: cc901875d53bf4e4fe0e01f02843871da4106e70


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker