CVE-2026-113209
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifios/web/navigation/crw_wk_navigation_handler.mm |
modified |
Files Changed
ios/web/web_state/ui/crw_web_controller_unittest.mmios/web/navigation/crw_wk_navigation_handler.mm
Patch
From 9088ac6b50ef9dd6d1d2943c737e851d7c288fd6 Mon Sep 17 00:00:00 2001
From: Mike Dougherty <michaeldo@chromium.org>
Date: Fri, 18 Sep 2026 11:10:26 -0700
Subject: [PATCH] Fix incorrect app specific iframe navigation check
This check must be done against the `targetFrame` of the navigation. The
previous logic was incorrect as checking the `sourceFrame` allows a web
page subframe to perform a top-level navigation to an app-specific URL
(e.g. via `top.location = 'chrome://...'`).
Additionally, fix existing tests which were unintentionally passing
because `VerifyDecidePolicyForNavigationAction` was unintentionally
modeling a main-frame navigation target rather than an iframe.
Fixed: 503774403
Change-Id: I69f9a47a6bb907b6c8beae0c15ee4e634269fb52
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8421650
Commit-Queue: Mike Dougherty <michaeldo@chromium.org>
Reviewed-by: Gauthier Ambard <gambard@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1700964}
---
diff --git a/ios/web/navigation/crw_wk_navigation_handler.mm b/ios/web/navigation/crw_wk_navigation_handler.mm
index 4aaaebd..0b20019 100644
--- a/ios/web/navigation/crw_wk_navigation_handler.mm
+++ b/ios/web/navigation/crw_wk_navigation_handler.mm
@@ -1559,9 +1559,9 @@
failedNavigationURL == _displayedErrorPageFailedNavigationURL;
}
- if (!action.sourceFrame.mainFrame) {
- // AppSpecific URLs are allowed inside iframe if the main frame is also
- // app specific page.
+ if (action.targetFrame && !action.targetFrame.mainFrame) {
+ // AppSpecific URLs are allowed inside an iframe only if the containing
+ // main document is also an app specific page.
GURL mainDocumentURL = net::GURLWithNSURL(action.request.mainDocumentURL);
if (web::GetWebClient()->IsAppSpecificURL(mainDocumentURL)) {
return YES;
diff --git a/ios/web/web_state/ui/crw_web_controller_unittest.mm b/ios/web/web_state/ui/crw_web_controller_unittest.mm
index 1be10e3..0146043 100644
--- a/ios/web/web_state/ui/crw_web_controller_unittest.mm
+++ b/ios/web/web_state/ui/crw_web_controller_unittest.mm
@@ -1067,14 +1067,16 @@
// timed out.
[[nodiscard]] bool VerifyDecidePolicyForNavigationAction(
NSURLRequest* request,
- WKNavigationActionPolicy expected_policy) {
+ WKNavigationActionPolicy expected_policy,
+ BOOL target_frame_is_main_frame = YES) {
CRWFakeWKNavigationAction* navigation_action =
[[CRWFakeWKNavigationAction alloc] init];
navigation_action.request = request;
- WKFrameInfo* mock_frame_info = OCMClassMock([WKFrameInfo class]);
- OCMStub([mock_frame_info isMainFrame]).andReturn(YES);
- navigation_action.targetFrame = mock_frame_info;
+ FakeWKFrameInfo* frame_info = [[FakeWKFrameInfo alloc] init];
+ frame_info.mainFrame = target_frame_is_main_frame;
+ frame_info.webView = mock_web_view_;
+ navigation_action.targetFrame = (WKFrameInfo*)frame_info;
WKWebpagePreferences* preferences = [[WKWebpagePreferences alloc] init];
@@ -1117,7 +1119,8 @@
[NSMutableURLRequest requestWithURL:app_url];
app_url_request.mainDocumentURL = app_url;
EXPECT_TRUE(VerifyDecidePolicyForNavigationAction(
- app_url_request, WKNavigationActionPolicyAllow));
+ app_url_request, WKNavigationActionPolicyAllow,
+ /*target_frame_is_main_frame=*/NO));
}
// Tests that URL is allowed in OffTheRecord mode when the
@@ -1167,7 +1170,46 @@
[NSMutableURLRequest requestWithURL:app_url];
app_url_request.mainDocumentURL = [NSURL URLWithString:@(kTestURLString)];
EXPECT_TRUE(VerifyDecidePolicyForNavigationAction(
- app_url_request, WKNavigationActionPolicyCancel));
+ app_url_request, WKNavigationActionPolicyCancel,
+ /*target_frame_is_main_frame=*/NO));
+}
+
+// Tests that a subframe cannot navigate the top-level main frame to an
+// App-specific URL even if the request's `mainDocumentURL` is set to the
+// destination URL.
+TEST_F(CRWWebControllerPolicyDeciderTest,
+ DisallowAppSpecificMainFrameNavigationFromIFrame) {
+ NSURL* app_url = [NSURL URLWithString:@(kTestAppSpecificURL)];
+ NSMutableURLRequest* app_url_request =
+ [NSMutableURLRequest requestWithURL:app_url];
+ // WebKit sets `mainDocumentURL` to destination URL for main-frame requests.
+ app_url_request.mainDocumentURL = app_url;
+
+ CRWFakeWKNavigationAction* action = [[CRWFakeWKNavigationAction alloc] init];
+ action.request = app_url_request;
+
+ FakeWKFrameInfo* target_frame = [[FakeWKFrameInfo alloc] init];
+ target_frame.mainFrame = YES;
+ target_frame.webView = mock_web_view_;
+ action.targetFrame = (WKFrameInfo*)target_frame;
+
+ FakeWKFrameInfo* source_frame = [[FakeWKFrameInfo alloc] init];
+ source_frame.mainFrame = NO;
+ source_frame.webView = mock_web_view_;
+ action.sourceFrame = (WKFrameInfo*)source_frame;
+
+ __block bool callback_called = false;
+ [navigation_delegate_ webView:mock_web_view_
+ decidePolicyForNavigationAction:action
+ preferences:[[WKWebpagePreferences alloc] init]
+ decisionHandler:^(WKNavigationActionPolicy policy,
+ WKWebpagePreferences* ignored) {
+ EXPECT_EQ(policy, WKNavigationActionPolicyCancel);
+ callback_called = true;
+ }];
+ EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^{
+ return callback_called;
+ }));
}
// Tests that blob URL navigation is allowed.
Regression Test / PoC
diff --git a/ios/web/web_state/ui/crw_web_controller_unittest.mm b/ios/web/web_state/ui/crw_web_controller_unittest.mm
index 1be10e3..0146043 100644
--- a/ios/web/web_state/ui/crw_web_controller_unittest.mm
+++ b/ios/web/web_state/ui/crw_web_controller_unittest.mm
@@ -1067,14 +1067,16 @@
// timed out.
[[nodiscard]] bool VerifyDecidePolicyForNavigationAction(
NSURLRequest* request,
- WKNavigationActionPolicy expected_policy) {
+ WKNavigationActionPolicy expected_policy,
+ BOOL target_frame_is_main_frame = YES) {
CRWFakeWKNavigationAction* navigation_action =
[[CRWFakeWKNavigationAction alloc] init];
navigation_action.request = request;
- WKFrameInfo* mock_frame_info = OCMClassMock([WKFrameInfo class]);
- OCMStub([mock_frame_info isMainFrame]).andReturn(YES);
- navigation_action.targetFrame = mock_frame_info;
+ FakeWKFrameInfo* frame_info = [[FakeWKFrameInfo alloc] init];
+ frame_info.mainFrame = target_frame_is_main_frame;
+ frame_info.webView = mock_web_view_;
+ navigation_action.targetFrame = (WKFrameInfo*)frame_info;
WKWebpagePreferences* preferences = [[WKWebpagePreferences alloc] init];
@@ -1117,7 +1119,8 @@
[NSMutableURLRequest requestWithURL:app_url];
app_url_request.mainDocumentURL = app_url;
EXPECT_TRUE(VerifyDecidePolicyForNavigationAction(
- app_url_request, WKNavigationActionPolicyAllow));
+ app_url_request, WKNavigationActionPolicyAllow,
+ /*target_frame_is_main_frame=*/NO));
}
// Tests that URL is allowed in OffTheRecord mode when the
@@ -1167,7 +1170,46 @@
[NSMutableURLRequest requestWithURL:app_url];
app_url_request.mainDocumentURL = [NSURL URLWithString:@(kTestURLString)];
EXPECT_TRUE(VerifyDecidePolicyForNavigationAction(
- app_url_request, WKNavigationActionPolicyCancel));
+ app_url_request, WKNavigationActionPolicyCancel,
+ /*target_frame_is_main_frame=*/NO));
+}
+
+// Tests that a subframe cannot navigate the top-level main frame to an
+// App-specific URL even if the request's `mainDocumentURL` is set to the
+// destination URL.
+TEST_F(CRWWebControllerPolicyDeciderTest,
+ DisallowAppSpecificMainFrameNavigationFromIFrame) {
+ NSURL* app_url = [NSURL URLWithString:@(kTestAppSpecificURL)];
+ NSMutableURLRequest* app_url_request =
+ [NSMutableURLRequest requestWithURL:app_url];
+ // WebKit sets `mainDocumentURL` to destination URL for main-frame requests.
+ app_url_request.mainDocumentURL = app_url;
+
+ CRWFakeWKNavigationAction* action = [[CRWFakeWKNavigationAction alloc] init];
+ action.request = app_url_request;
+
+ FakeWKFrameInfo* target_frame = [[FakeWKFrameInfo alloc] init];
+ target_frame.mainFrame = YES;
+ target_frame.webView = mock_web_view_;
+ action.targetFrame = (WKFrameInfo*)target_frame;
+
+ FakeWKFrameInfo* source_frame = [[FakeWKFrameInfo alloc] init];
+ source_frame.mainFrame = NO;
+ source_frame.webView = mock_web_view_;
+ action.sourceFrame = (WKFrameInfo*)source_frame;
+
+ __block bool callback_called = false;
+ [navigation_delegate_ webView:mock_web_view_
+ decidePolicyForNavigationAction:action
+ preferences:[[WKWebpagePreferences alloc] init]
+ decisionHandler:^(WKNavigationActionPolicy policy,
+ WKWebpagePreferences* ignored) {
+ EXPECT_EQ(policy, WKNavigationActionPolicyCancel);
+ callback_called = true;
+ }];
+ EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^{
+ return callback_called;
+ }));
}
// Tests that blob URL navigation is allowed.
Original Bug Report
WebUI navigation restriction bypass via subframe-initiated top-level navigation on iOS
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 without the Chrome Security team. Please see go/chrome-ai-generated-security-bugs-faq for more information.
Overview: A logic error in shouldAllowAppSpecificURLNavigationAction on iOS allows a web-controlled iframe to initiate a top-level navigation to a chrome:// URL. This bypasses security checks intended to prevent untrusted web content from triggering navigations to privileged App-specific pages.
Affected files:
ios/web/navigation/crw_wk_navigation_handler.mmios/web/web_state/ui/crw_web_controller_unittest.mm
Estimated timestamp from git blame: 2025-09-11
Description
A logic error exists in the iOS implementation of WebUI navigation restrictions within ios/web/navigation/crw_wk_navigation_handler.mm. The function shouldAllowAppSpecificURLNavigationAction:transition: contains a clause intended to allow chrome:// (App-specific) URLs to load within an iframe, provided the parent page is already an App-specific page.
However, the implementation incorrectly checks !action.sourceFrame.mainFrame (verifying if the initiator is an iframe) and then relies on action.request.mainDocumentURL. WebKit populates mainDocumentURL with the destination URL for top-level navigations.
When a subframe executes top.location = 'chrome://version', the following occurs:
action.sourceFrame.mainFrameisNObecause the navigation was initiated by a subframe.- Since this is a top-level navigation, WebKit sets
action.request.mainDocumentURLto the destination (chrome://version). shouldAllowAppSpecificURLNavigationActionsees that the initiator is not the main frame and that themainDocumentURLis an App-specific URL, so it erroneously returnsYES.
This allows an untrusted website to force a top-level navigation to a highly privileged chrome:// URL, violating the invariant that Chromium does not allow running App-specific pages in the same process or context as an untrusted website. While the Same-Origin Policy (SOP) prevents the attacker from directly scripting the resulting WebUI document, this vulnerability serves as a defense-in-depth bypass and could be chained with other vulnerabilities.
Flawed Unit Test
The existing unit test AllowAppSpecificIFrameFromAppSpecificPage in ios/web/web_state/ui/crw_web_controller_unittest.mm fails to catch this. It advertently mocks a top-level navigation (targetFrame.mainFrame = YES with sourceFrame = nil) while intending to test subframe behavior. Because the test sets a chrome:// URL as the mainDocumentURL, it triggers the buggy logic path and passes, validating the insecure behavior.
Potential Reproduction Steps
Note: These are suggested steps based on code analysis; a functional exploit has not been executed.
- Host a page at
http://attacker.test/index.htmlcontaining an iframe. - From within the iframe, execute:
top.location = 'chrome://version';. (IfkPreventNavigationWithoutUserInteractionis enabled, anonclickhandler inside the iframe can be used to trigger the navigation with a valid user gesture). - Observe that Chrome for iOS allows the navigation and loads the
chrome://versionpage in the top-level frame.
Suggested Fix
In shouldAllowAppSpecificURLNavigationAction:transition:, the logic intended to allow App-specific iframes within App-specific pages should verify that the target frame is not the main frame (!action.targetFrame.mainFrame). Furthermore, it should validate the actual URL or origin of the main frame containing the iframe, rather than relying on action.request.mainDocumentURL, which is unreliable for top-level navigations.
Evaluated with Chrome root at commit: 661452647ddb2827305122ff3273bd5dea403f09
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.