CVE-2026-13813
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fios/web/webui/crw_web_ui_scheme_handler_unittest.mm |
modified |
Files Changed
ios/web/webui/crw_web_ui_scheme_handler.mmios/web/webui/crw_web_ui_scheme_handler_unittest.mm
Patch
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), "{}");
Regression Test / PoC
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), "{}");
Original Bug Report
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.mmios/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:
- WebUI Validation: It verifies that the
mainDocumentURLof the request belongs to a recognized WebUI origin (e.g.,chrome://sync-internals). - 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
- An attacker achieves arbitrary code execution within the WebKit WebContent (renderer) process.
- The attacker executes
window.open()to create a newWKWebViewin the browser process. At this moment, theWKWebView’sURLproperty isnil. - Before any navigation commits in the new window, the attacker’s renderer initiates a request (e.g., via
fetch) for a sensitive resource likechrome://prefs-internals. - The attacker spoofs the IPC payload, setting the request’s
mainDocumentURLto a valid WebUI origin likechrome://sync-internals. - The browser process receives the request in
CRWWebUISchemeHandler. - The first check passes because
chrome://sync-internalsis a valid WebUI. - The cross-origin check evaluates
webView.URL. Since it isnil, the check short-circuits, completely bypassing the security restriction. URLFetcherBlockAdapterexecutes the request using the browser’s trustedSharedURLLoaderFactoryand 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.