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
Tracker516894682
Fix commit172a793d3c21 (chromium/src) +95/-4
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

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

Files Changed

  • ios/web/BUILD.gn
  • ios/web/webui/crw_web_ui_scheme_handler.mm
  • ios/web/webui/crw_web_ui_scheme_handler_unittest.mm
From 172a793d3c216d66b1e6906189da43044a8d2de0 Mon Sep 17 00:00:00 2001
From: Mike Dougherty <[email protected]>
Date: Mon, 22 Jun 2026 22:41:49 -0700
Subject: [PATCH] Prevent provisional navigations from accessing WebUI resources

Fixed: 516894682
Change-Id: I44d8345d958675b31c1312e88bed966ac69b460b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7979842
Reviewed-by: Guillem Perez <[email protected]>
Commit-Queue: Mike Dougherty <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1650780}
---

diff --git a/ios/web/BUILD.gn b/ios/web/BUILD.gn
index b432f3b0..91e448d 100644
--- a/ios/web/BUILD.gn
+++ b/ios/web/BUILD.gn
@@ -491,6 +491,7 @@
     "//components/url_formatter",
     "//ios/net",
     "//ios/testing:ocmock_support",
+    "//ios/web/js_messaging",
     "//ios/web/public/test",
     "//ios/web/public/test:test_fixture",
     "//ios/web/public/test/fakes",
diff --git a/ios/web/webui/crw_web_ui_scheme_handler.mm b/ios/web/webui/crw_web_ui_scheme_handler.mm
index b9efad1..47544e3e 100644
--- a/ios/web/webui/crw_web_ui_scheme_handler.mm
+++ b/ios/web/webui/crw_web_ui_scheme_handler.mm
@@ -9,6 +9,8 @@
 
 #import "base/files/file_path.h"
 #import "base/strings/sys_string_conversions.h"
+#import "ios/web/js_messaging/web_view_web_state_map.h"
+#import "ios/web/public/web_state.h"
 #import "ios/web/webui/url_fetcher_block_adapter.h"
 #import "ios/web/webui/web_ui_constants.h"
 #import "ios/web/webui/web_ui_ios_controller_factory_registry.h"
@@ -64,10 +66,23 @@
   // 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 (!webView.URL ||
-      (!URL.DomainIs(web::kWebUIResourcesHost) &&
-       url::SchemeHostPort(URL) !=
-           url::SchemeHostPort(net::GURLWithNSURL(webView.URL)))) {
+
+  // Allow the main-frame navigation request itself (its URL matches the
+  // provisional webView.URL).
+  // Subresource requests are gated on the WebState's last committed URL
+  // because both `webView.URL` and `backForwardList.currentItem.URL` may
+  // already point at the destination of an in-progress navigation. The main
+  // document request itself is loaded before commit and is identified by
+  // matching `webView.URL`.
+  GURL webViewURL = net::GURLWithNSURL(webView.URL);
+  web::WebState* webState = web::GetWebStateForWebView(webView);
+  GURL committedURL = webState ? webState->GetLastCommittedURL() : GURL();
+  BOOL isMainDocumentRequest = URL.EqualsIgnoringRef(webViewURL);
+  BOOL isSharedResourceRequest = URL.DomainIs(web::kWebUIResourcesHost) &&
+                                 GetErrorCodeForUrl(committedURL) == 0;
+  if (!webState || !webViewURL.is_valid() ||
+      (!isMainDocumentRequest && !isSharedResourceRequest &&
+       url::SchemeHostPort(URL) != url::SchemeHostPort(committedURL))) {
     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 ad39a59..ea9a35b 100644
--- a/ios/web/webui/crw_web_ui_scheme_handler_unittest.mm
+++ b/ios/web/webui/crw_web_ui_scheme_handler_unittest.mm
@@ -7,6 +7,8 @@
 #import "base/run_loop.h"
 #import "base/strings/sys_string_conversions.h"
 #import "base/test/ios/wait_util.h"
+#import "ios/web/js_messaging/web_view_web_state_map.h"
+#import "ios/web/public/test/fakes/fake_web_state.h"
 #import "ios/web/public/test/web_test.h"
 #import "ios/web/public/webui/web_ui_ios_controller.h"
 #import "ios/web/public/webui/web_ui_ios_controller_factory.h"
@@ -147,6 +149,10 @@
   request.mainDocumentURL = GetWebUIURL();
   OCMStub([web_view URL]).andReturn(request.mainDocumentURL);
 
+  web::FakeWebState web_state;
+  web_state.SetCurrentURL(net::GURLWithNSURL(request.mainDocumentURL));
+  web::SetAssociatedWebViewForWebState(web_view, &web_state);
+
   url_scheme_task.request = request;
 
   [scheme_handler webView:web_view startURLSchemeTask:url_scheme_task];
@@ -154,6 +160,8 @@
   RespondWithData(net::GURLWithNSURL(request.URL), "{}");
   EXPECT_TRUE(url_scheme_task.receivedData);
   EXPECT_FALSE(url_scheme_task.receivedError);
+
+  web::ClearAssociatedWebViewForWebState(web_view, &web_state);
 }
 
 // Tests that the error returned is the same as the error from the factory.
@@ -170,6 +178,10 @@
   url_scheme_task.request = request;
   OCMStub([web_view URL]).andReturn(request.URL);
 
+  web::FakeWebState web_state;
+  web_state.SetCurrentURL(net::GURLWithNSURL(request.mainDocumentURL));
+  web::SetAssociatedWebViewForWebState(web_view, &web_state);
+
   [scheme_handler webView:web_view startURLSchemeTask:url_scheme_task];
 
   RespondWithData(net::GURLWithNSURL(request.URL), "{}");
@@ -181,12 +193,17 @@
   request.mainDocumentURL = [NSURL URLWithString:@"invalidScheme://page"];
   OCMStub([web_view URL]).andReturn(request.mainDocumentURL);
   url_scheme_task.request = request;
+
+  web_state.SetCurrentURL(net::GURLWithNSURL(request.mainDocumentURL));
+
   [scheme_handler webView:web_view startURLSchemeTask:url_scheme_task];
 
   RespondWithData(net::GURLWithNSURL(request.URL), "{}");
   EXPECT_FALSE(url_scheme_task.receivedData);
   EXPECT_TRUE(url_scheme_task.receivedError);
   EXPECT_EQ(NSURLErrorUnsupportedURL, url_scheme_task.error.code);
+
+  web::ClearAssociatedWebViewForWebState(web_view, &web_state);
 }
 
 // Tests that calling start on the scheme handler returns an error when the URL
@@ -201,11 +218,57 @@
   OCMStub([web_view URL]).andReturn(request.mainDocumentURL);
   url_scheme_task.request = request;
 
+  web::FakeWebState web_state;
+  web_state.SetCurrentURL(net::GURLWithNSURL(request.mainDocumentURL));
+  web::SetAssociatedWebViewForWebState(web_view, &web_state);
+
   [scheme_handler webView:web_view startURLSchemeTask:url_scheme_task];
 
   RespondWithData(net::GURLWithNSURL(request.URL), "{}");
   EXPECT_FALSE(url_scheme_task.receivedData);
   EXPECT_TRUE(url_scheme_task.receivedError);
+
+  web::ClearAssociatedWebViewForWebState(web_view, &web_state);
+}
+
+// Tests that same-origin logic blocks subresource requests from non-WebUI pages
+// during provisional navigation. See crbug.com/516894682 for more details.
+TEST_F(CRWWebUISchemeManagerTest, WebUISourcesBlockedDuringProvisionalLoads) {
+  CRWWebUISchemeHandler* scheme_handler = CreateSchemeHandler();
+
+  // State during navigation: committed document is https://evil.com,
+  // but provisional webView.URL is chrome://version/
+  NSURL* committed_url = [NSURL URLWithString:@"https://evil.com/"];
+  NSURL* provisional_url = [NSURL URLWithString:@"chrome://version/"];
+
+  id current_item = OCMClassMock([WKBackForwardListItem class]);
+  OCMStub([current_item URL]).andReturn(committed_url);
+  id bf_list = OCMClassMock([WKBackForwardList class]);
+  OCMStub([bf_list currentItem]).andReturn(current_item);
+  id web_view = OCMClassMock([WKWebView class]);
+  OCMStub([web_view backForwardList]).andReturn(bf_list);
+  OCMStub([web_view URL]).andReturn(provisional_url);
+
+  web::FakeWebState web_state;
+  web_state.SetCurrentURL(net::GURLWithNSURL(committed_url));
+  web::SetAssociatedWebViewForWebState(web_view, &web_state);
+
+  // Attacker-crafted subresource task from evil.com targeting chrome://version
+  FakeSchemeTask* url_scheme_task = [[FakeSchemeTask alloc] init];
+  NSMutableURLRequest* request = [NSMutableURLRequest
+      requestWithURL:[NSURL URLWithString:@"chrome://version/version.js"]];
+  request.mainDocumentURL = [NSURL URLWithString:@"chrome://version/"];
+  url_scheme_task.request = request;
+
+  [scheme_handler webView:web_view startURLSchemeTask:url_scheme_task];
+  RespondWithData(net::GURLWithNSURL(request.URL), "SECRET-WEBUI-PAYLOAD");
+
+  // The request should be blocked.
+  EXPECT_FALSE(url_scheme_task.receivedData);
+  EXPECT_TRUE(url_scheme_task.receivedError);
+  EXPECT_EQ(NSURLErrorNoPermissionsToReadFile, url_scheme_task.error.code);
+
+  web::ClearAssociatedWebViewForWebState(web_view, &web_state);
 }
 
 // Tests that calling stop right after start prevent the handler from returning
@@ -220,12 +283,18 @@
   OCMStub([web_view URL]).andReturn(request.mainDocumentURL);
   url_scheme_task.request = request;
 
+  web::FakeWebState web_state;
+  web_state.SetCurrentURL(net::GURLWithNSURL(request.mainDocumentURL));
+  web::SetAssociatedWebViewForWebState(web_view, &web_state);
+
   [scheme_handler webView:web_view startURLSchemeTask:url_scheme_task];
   [scheme_handler webView:web_view stopURLSchemeTask:url_scheme_task];
 
   RespondWithData(net::GURLWithNSURL(request.URL), "{}");
   EXPECT_FALSE(url_scheme_task.receivedData);
   EXPECT_FALSE(url_scheme_task.receivedError);
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 ad39a59..ea9a35b 100644
--- a/ios/web/webui/crw_web_ui_scheme_handler_unittest.mm
+++ b/ios/web/webui/crw_web_ui_scheme_handler_unittest.mm
@@ -7,6 +7,8 @@
 #import "base/run_loop.h"
 #import "base/strings/sys_string_conversions.h"
 #import "base/test/ios/wait_util.h"
+#import "ios/web/js_messaging/web_view_web_state_map.h"
+#import "ios/web/public/test/fakes/fake_web_state.h"
 #import "ios/web/public/test/web_test.h"
 #import "ios/web/public/webui/web_ui_ios_controller.h"
 #import "ios/web/public/webui/web_ui_ios_controller_factory.h"
@@ -147,6 +149,10 @@
   request.mainDocumentURL = GetWebUIURL();
   OCMStub([web_view URL]).andReturn(request.mainDocumentURL);
 
+  web::FakeWebState web_state;
+  web_state.SetCurrentURL(net::GURLWithNSURL(request.mainDocumentURL));
+  web::SetAssociatedWebViewForWebState(web_view, &web_state);
+
   url_scheme_task.request = request;
 
   [scheme_handler webView:web_view startURLSchemeTask:url_scheme_task];
@@ -154,6 +160,8 @@
   RespondWithData(net::GURLWithNSURL(request.URL), "{}");
   EXPECT_TRUE(url_scheme_task.receivedData);
   EXPECT_FALSE(url_scheme_task.receivedError);
+
+  web::ClearAssociatedWebViewForWebState(web_view, &web_state);
 }
 
 // Tests that the error returned is the same as the error from the factory.
@@ -170,6 +178,10 @@
   url_scheme_task.request = request;
   OCMStub([web_view URL]).andReturn(request.URL);
 
+  web::FakeWebState web_state;
+  web_state.SetCurrentURL(net::GURLWithNSURL(request.mainDocumentURL));
+  web::SetAssociatedWebViewForWebState(web_view, &web_state);
+
   [scheme_handler webView:web_view startURLSchemeTask:url_scheme_task];
 
   RespondWithData(net::GURLWithNSURL(request.URL), "{}");
@@ -181,12 +193,17 @@
   request.mainDocumentURL = [NSURL URLWithString:@"invalidScheme://page"];
   OCMStub([web_view URL]).andReturn(request.mainDocumentURL);
   url_scheme_task.request = request;
+
+  web_state.SetCurrentURL(net::GURLWithNSURL(request.mainDocumentURL));
+
   [scheme_handler webView:web_view startURLSchemeTask:url_scheme_task];
 
   RespondWithData(net::GURLWithNSURL(request.URL), "{}");
   EXPECT_FALSE(url_scheme_task.receivedData);
   EXPECT_TRUE(url_scheme_task.receivedError);
   EXPECT_EQ(NSURLErrorUnsupportedURL, url_scheme_task.error.code);
+
+  web::ClearAssociatedWebViewForWebState(web_view, &web_state);
 }
 
 // Tests that calling start on the scheme handler returns an error when the URL
@@ -201,11 +218,57 @@
   OCMStub([web_view URL]).andReturn(request.mainDocumentURL);
   url_scheme_task.request = request;
 
+  web::FakeWebState web_state;
+  web_state.SetCurrentURL(net::GURLWithNSURL(request.mainDocumentURL));
+  web::SetAssociatedWebViewForWebState(web_view, &web_state);
+
   [scheme_handler webView:web_view startURLSchemeTask:url_scheme_task];
 
   RespondWithData(net::GURLWithNSURL(request.URL), "{}");
   EXPECT_FALSE(url_scheme_task.receivedData);
   EXPECT_TRUE(url_scheme_task.receivedError);
+
+  web::ClearAssociatedWebViewForWebState(web_view, &web_state);
+}
+
+// Tests that same-origin logic blocks subresource requests from non-WebUI pages
+// during provisional navigation. See crbug.com/516894682 for more details.
+TEST_F(CRWWebUISchemeManagerTest, WebUISourcesBlockedDuringProvisionalLoads) {
+  CRWWebUISchemeHandler* scheme_handler = CreateSchemeHandler();
+
+  // State during navigation: committed document is https://evil.com,
+  // but provisional webView.URL is chrome://version/
+  NSURL* committed_url = [NSURL URLWithString:@"https://evil.com/"];
+  NSURL* provisional_url = [NSURL URLWithString:@"chrome://version/"];
+
+  id current_item = OCMClassMock([WKBackForwardListItem class]);
+  OCMStub([current_item URL]).andReturn(committed_url);
+  id bf_list = OCMClassMock([WKBackForwardList class]);
+  OCMStub([bf_list currentItem]).andReturn(current_item);
+  id web_view = OCMClassMock([WKWebView class]);
+  OCMStub([web_view backForwardList]).andReturn(bf_list);
+  OCMStub([web_view URL]).andReturn(provisional_url);
+
+  web::FakeWebState web_state;
+  web_state.SetCurrentURL(net::GURLWithNSURL(committed_url));
+  web::SetAssociatedWebViewForWebState(web_view, &web_state);
+
+  // Attacker-crafted subresource task from evil.com targeting chrome://version
+  FakeSchemeTask* url_scheme_task = [[FakeSchemeTask alloc] init];
+  NSMutableURLRequest* request = [NSMutableURLRequest
+      requestWithURL:[NSURL URLWithString:@"chrome://version/version.js"]];
+  request.mainDocumentURL = [NSURL URLWithString:@"chrome://version/"];
+  url_scheme_task.request = request;
+
+  [scheme_handler webView:web_view startURLSchemeTask:url_scheme_task];
+  RespondWithData(net::GURLWithNSURL(request.URL), "SECRET-WEBUI-PAYLOAD");
+
+  // The request should be blocked.
+  EXPECT_FALSE(url_scheme_task.receivedData);
+  EXPECT_TRUE(url_scheme_task.receivedError);
+  EXPECT_EQ(NSURLErrorNoPermissionsToReadFile, url_scheme_task.error.code);
+
+  web::ClearAssociatedWebViewForWebState(web_view, &web_state);
 }
 
 // Tests that calling stop right after start prevent the handler from returning
@@ -220,12 +283,18 @@
   OCMStub([web_view URL]).andReturn(request.mainDocumentURL);
   url_scheme_task.request = request;
 
+  web::FakeWebState web_state;
+  web_state.SetCurrentURL(net::GURLWithNSURL(request.mainDocumentURL));
+  web::SetAssociatedWebViewForWebState(web_view, &web_state);
+
   [scheme_handler webView:web_view startURLSchemeTask:url_scheme_task];
   [scheme_handler webView:web_view stopURLSchemeTask:url_scheme_task];
 
   RespondWithData(net::GURLWithNSURL(request.URL), "{}");
   EXPECT_FALSE(url_scheme_task.receivedData);
   EXPECT_FALSE(url_scheme_task.receivedError);
+
+  web::ClearAssociatedWebViewForWebState(web_view, &web_state);
 }
 
 // Tests that proper mime-type is returned for a given chrome:// request.
@@ -234,6 +303,10 @@
   id web_view = OCMClassMock([WKWebView class]);
   FakeSchemeTask* url_scheme_task = [[FakeSchemeTask alloc] init];
 
+  web::FakeWebState web_state;
+  web_state.SetCurrentURL(GURL("chrome://clown/"));
+  web::SetAssociatedWebViewForWebState(web_view, &web_state);
+
   // Check javascript
   NSMutableURLRequest* request = [NSMutableURLRequest
       requestWithURL:[NSURL URLWithString:@"chrome://clown/res/clown.js"]];
@@ -286,6 +359,8 @@
   EXPECT_TRUE([url_scheme_task responseHasMimetype:@"text/html"]);
   EXPECT_TRUE(url_scheme_task.receivedData);
   EXPECT_FALSE(url_scheme_task.receivedError);
+
+  web::ClearAssociatedWebViewForWebState(web_view, &web_state);
 }
 
 }  // namespace web
Loading diff…

Original Bug Report

reported by [email protected]

Potential same-origin bypass in CRWWebUISchemeHandler via provisional webView.URL

Project Fortify, 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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A potential same-origin bypass in iOS Web allows a compromised WebContent process to bypass security gates in CRWWebUISchemeHandler. By programmatically initiating a back-forward navigation to a privileged WebUI page, an attacker can exploit a race window where webView.URL updates to the destination URL while the untrusted document is still actively executing. This allows the untrusted document to successfully load and exfiltrate highly privileged WebUI resources.

Affected files:

  • ios/web/webui/crw_web_ui_scheme_handler.mm
  • ios/web/navigation/crw_wk_navigation_handler.mm
  • ios/web/navigation/crw_web_view_navigation_observer.mm

Estimated timestamp from git blame: 2026-04-27

Root Cause Analysis

In -[CRWWebUISchemeHandler webView:startURLSchemeTask:] (ios/web/webui/crw_web_ui_scheme_handler.mm, lines 67-70), same-origin validation for WKURLSchemeTask subresource requests is performed against webView.URL:

if (!webView.URL ||
    (!URL.DomainIs(web::kWebUIResourcesHost) &&
     url::SchemeHostPort(URL) !=
         url::SchemeHostPort(net::GURLWithNSURL(webView.URL)))) {
  // deny and fail scheme task
  ...
}

However, under iOS WKWebView navigation flows, webView.URL is updated to the destination target URL during the provisional phase of a navigation, before the old document is swapped out. This platform behavior is documented inside Chromium’s navigation observer (ios/web/navigation/crw_web_view_navigation_observer.mm, lines 275-278):

// URL changes happen at four points:
// 1) When a load starts; at this point, the load is provisional, and
//    it should be ignored until it's committed, since the document/window
//    objects haven't changed yet.

Because the previous document has not been swapped out and remains fully active during this provisional phase, a compromised renderer can issue a WKURLSchemeTask whose origin will be incorrectly validated against the target provisional URL instead of the active document’s true committed origin.

Furthermore, back-forward navigations to app-specific (WebUI) URLs are unconditionally permitted inside shouldAllowAppSpecificURLNavigationAction:transition: (ios/web/navigation/crw_wk_navigation_handler.mm, lines 1436-1439):

if (pageTransition & ui::PAGE_TRANSITION_FORWARD_BACK) {
  // Allow back-forward navigations.
  return YES;
}

Potential Attack Scenario / Steps to Trigger

An attacker who has achieved remote code execution (RCE) inside the sandboxed WebContent process could potentially perform the following steps to trigger this vulnerability:

  1. Precondition: The active tab has previously visited a WebUI page (such as chrome://version or chrome://newtab), leaving it in the back-forward history stack.
  2. Initiating Navigation: The compromised WebContent process programmatically triggers a back-forward navigation via window.history.back() or similar.
  3. Provisional State Update: The navigation begins, and Apple’s WKWebView immediately updates its URL property to the target destination (e.g., chrome://version). The untrusted document (e.g., https://evil.com) remains actively executing.
  4. Race Request: Within this race window, the compromised WebContent process issues subresource fetch requests targeting the same WebUI host (chrome://version/...).
  5. Validation Bypass: The security gate in CRWWebUISchemeHandler compares the subresource origin against webView.URL. Since webView.URL has already transitioned to chrome://version, the validation check passes.
  6. Exfiltration: The unsandboxed UI process fetches the privileged resource and returns it directly to the compromised WebContent process.

Note: These are potential steps based on source code analysis; our tooling agent does not currently have the ability to run functional exploit code on this target platform.

Impact

This issue constitutes a potential sandbox escape / cross-origin data leak. A compromised sandboxed WebContent process can bypass the origin gate and use the UI process as a confused deputy to fetch and exfiltrate highly privileged WebUI resource files (such as version details, diagnostic configurations, or internal assets) without authorization.

Suggested Fix

CRWWebUISchemeHandler must perform origin validation against a trusted anchor representing the last committed document URL rather than the provisional webView.URL. This can be achieved by retrieving the committed URL from the corresponding WebState navigation history, or validating against webView.backForwardList.currentItem.URL.

Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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
Links in the report