High watch CVSS 8.3 chrome Authorization Error 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in Navigation
DescriptionIncorrect authorization in Navigation
ComponentNavigation
Sub-componentiOS
Bug ClassAuthorization 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.
Tracker508462481
Fix commitbb617c8b09d4 (chromium/src) +138/-4
CWECWE-863
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-05-01 (per vendor advisory)
Fix landed2026-08-21 (public git)
Disclosed2026-09-22
Patch gap32 days — fix public in source before the advisory

Background

`CRWWKNavigationHandler`
the iOS //ios/web class that implements the WKNavigationDelegate protocol and decides, per navigation, whether WKWebView may proceed.
Error page file URL
a local file:// URL (identified by [CRWErrorPageHelper isErrorPageFileURL:]) that renders Chrome’s committed error page for a failed navigation.
`WKNavigationTypeBackForward` / `WKNavigationTypeReload`
WebKit navigation-type flags marking a navigation as a history traversal or reload of an already-committed item rather than a fresh, script-initiable load.
`native_restore_in_progress_`
the NavigationManagerImpl flag, exposed by the new IsRestoreSessionInProgress(), that is set only while the system is restoring a saved session.

Root Cause Analysis

In -[CRWWKNavigationHandler webView:decidePolicyForNavigationAction:decisionHandler:], any navigation whose target URL satisfied [CRWErrorPageHelper isErrorPageFileURL:requestURL] was unconditionally allowed with WKNavigationActionPolicyAllow, with no check on who initiated it. Because the local error page is a real file:// document that Chrome grants read access to, a renderer- or page-initiated navigation to such a URL was treated as trusted, violating the invariant that error page file URLs may only be loaded under browser control.

The fix records the exact URL the browser intends to load in a new allowedErrorPageFileURL property and only permits an error page navigation when it matches that expected URL in the main frame, or when it is a genuine WKNavigationTypeBackForward / WKNavigationTypeReload of a committed page, or while IsRestoreSessionInProgress() is true; everything else is now WKNavigationActionPolicyCancel. It additionally tightens the outbound path so navigations originating from an error page are only allowed back to the failed URL that error page represents. Together these gates ensure error page file URLs are loaded only in contexts the browser explicitly authorized.

Key insight
The single mistake was authorizing a navigation purely on the destination URL’s shape (that it looks like an error page file URL) instead of on its provenance, so any context could pull up the local error page document; the fix binds each error page load to a browser-set expected URL plus a small allowlist of safe navigation types, making provenance rather than URL pattern the authorization check.

Attack Path

  1. Reach the unguarded allow branch A page or renderer-controlled navigation targets a URL that [CRWErrorPageHelper isErrorPageFileURL:] recognizes, hitting the branch that previously returned WKNavigationActionPolicyAllow unconditionally.
  2. Load the local error page file URL Because no initiator or expected-URL check existed, WKWebView commits the local file:// error page document even though the browser never requested that specific load.
  3. Abuse the error page's privileged position (inferred) A committed error page document has an outbound allowance for onward navigation (the isErrorPageFileURL:self.documentURL path), which an attacker-controlled error page context could try to leverage toward otherwise-restricted URLs; the fix now constrains this to only the failed URL the error page represents.
  4. Escalate to an authorization bypass in navigation (inferred) The net effect the advisory describes is incorrect authorization in Navigation, i.e. performing a navigation state or context the origin should not be able to reach; the precise downstream primitive is not shown by the diff.

Impact Assessment

An attacker able to trigger navigations in the WebKit-backed browser context on iOS could cause the local error page file URL to load without browser authorization, resulting in incorrect authorization within Navigation (bug 508462481). The preconditions are that the attacker controls or influences a navigation and that it is neither a legitimate back/forward, reload, nor session restore; further escalation beyond loading the error page document is (inferred) and not directly demonstrated by the patch. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.3.

Changed Functions

FunctionChangeNotes
if
ios/web/navigation/crw_wk_navigation_handler.mm
modified
if
ios/web/navigation/navigation_manager_impl.mm
modified
TEST_F
ios/web/web_state/ui/crw_web_controller_unittest.mm
modified
WindowOpenByDomTest
ios/web/web_state/ui/crw_web_controller_unittest.mm
modified

Files Changed

  • ios/web/navigation/navigation_manager_impl.mm
  • ios/web/web_state/ui/crw_web_controller_unittest.mm
  • ios/web/navigation/navigation_manager_impl.h
  • ios/web/navigation/crw_wk_navigation_handler.mm

Audit Directions

  • URL-shape authorization
    Audit navigation delegates that grant Allow based on a destination URL predicate (scheme, file:// error/WebUI paths) rather than the navigation’s initiator or type, especially unconditional early-return allow branches.
  • Dangling trust state
    Review one-shot “expected URL” or capability tokens like allowedErrorPageFileURL to confirm they are cleared on every path (main-frame navigations, HTML-string loads) so a stale value cannot authorize a later unrelated load.
  • Error/interstitial page privilege
    Inspect what committed error, interstitial, or WebUI documents are permitted to navigate to, ensuring onward navigations are constrained to the specific failed URL they represent rather than a broad category.
From bb617c8b09d43ed4d617d0c2b6769899324c9ee8 Mon Sep 17 00:00:00 2001
From: Justin Cohen <justincohen@google.com>
Date: Fri, 21 Aug 2026 12:07:28 -0700
Subject: [PATCH] ios: Validate error page navigations in CRWWKNavigationHandler.

Error page navigations inside CRWWKNavigationHandler can currently be
initiated by any context. This change ensures that navigations to local
error page file URLs are only allowed if explicitly initiated by the
browser process, or if they are safe back/forward or reload navigations
of already committed pages.

It also allows error page loads during active session restoration,
which is securely managed by the system.

Bug: 508462481
Change-Id: Ic17c3eaa553b80ca5a62de4ab0178be38a5de8b4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7862880
Auto-Submit: Justin Cohen <justincohen@google.com>
Reviewed-by: Matt Reichhoff <mreichhoff@chromium.org>
Reviewed-by: Mike Dougherty <michaeldo@chromium.org>
Commit-Queue: Mike Dougherty <michaeldo@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1684115}
---
diff --git a/ios/web/navigation/crw_wk_navigation_handler.mm b/ios/web/navigation/crw_wk_navigation_handler.mm
index 2956871..716679ef 100644
--- a/ios/web/navigation/crw_wk_navigation_handler.mm
+++ b/ios/web/navigation/crw_wk_navigation_handler.mm
@@ -179,6 +179,7 @@
 }
 
 @property(nonatomic, weak) id<CRWWKNavigationHandlerDelegate> delegate;
+@property(nonatomic, copy) NSURL* allowedErrorPageFileURL;
 
 // Returns the WebStateImpl from self.delegate.
 @property(nonatomic, readonly, assign) web::WebStateImpl* webStateImpl;
@@ -331,9 +332,31 @@
     }
   }
 
-  // If this is a error navigation, pass through.
+  // Error pages must only be loaded in the main frame. Unconditionally clear
+  // the allowed error page URL on any main frame navigation to prevent dangling
+  // states (e.g., if a load HTML string request was made instead of a
+  // file URL load).
+  NSURL* allowedErrorPageFileURL = self.allowedErrorPageFileURL;
+  if (action.targetFrame.mainFrame) {
+    self.allowedErrorPageFileURL = nil;
+  }
+
+  // If this is an error navigation, pass through only if initiated by the
+  // browser, or if it is a back/forward or reload of an already committed page.
   if ([CRWErrorPageHelper isErrorPageFileURL:requestURL]) {
-    decisionHandler(WKNavigationActionPolicyAllow);
+    BOOL isHistoryOrReloadOrRestore =
+        action.navigationType == WKNavigationTypeBackForward ||
+        action.navigationType == WKNavigationTypeReload ||
+        self.navigationManagerImpl->IsRestoreSessionInProgress();
+    BOOL isExpectedErrorPage =
+        allowedErrorPageFileURL &&
+        requestURL == net::GURLWithNSURL(allowedErrorPageFileURL);
+    if ((isExpectedErrorPage && action.targetFrame.mainFrame) ||
+        isHistoryOrReloadOrRestore) {
+      decisionHandler(WKNavigationActionPolicyAllow);
+      return;
+    }
+    decisionHandler(WKNavigationActionPolicyCancel);
     return;
   }
 
@@ -1483,9 +1506,12 @@
     }
   }
 
-  // Allow navigation to WebUI pages from error pages.
+  // Allow navigation from an error page to the failed URL it represents so
+  // that error pages for app-specific URLs can retry the original navigation.
   if ([CRWErrorPageHelper isErrorPageFileURL:self.documentURL]) {
-    return YES;
+    return requestURL ==
+           [CRWErrorPageHelper
+               failedNavigationURLFromErrorPageFileURL:self.documentURL];
   }
 
   if (!action.sourceFrame.mainFrame) {
@@ -2144,6 +2170,7 @@
           isErrorPageFileURLForFailedNavigationURL:backForwardItem.URL] &&
       !isSameURLFromWebClient &&
       ![backForwardItem.URL isEqual:errorPage.failedNavigationURL]) {
+    self.allowedErrorPageFileURL = errorPage.errorPageFileURL;
     errorNavigation = [webView loadFileURL:errorPage.errorPageFileURL
                    allowingReadAccessToURL:errorPage.errorPageFileURL];
   } else {
diff --git a/ios/web/navigation/navigation_manager_impl.h b/ios/web/navigation/navigation_manager_impl.h
index d71cde27..0951dbe 100644
--- a/ios/web/navigation/navigation_manager_impl.h
+++ b/ios/web/navigation/navigation_manager_impl.h
@@ -208,6 +208,7 @@
   // TODO(crbug.com/41284081): Make this private once all navigation code is
   // moved out of CRWWebController.
   NavigationItemImpl* GetCurrentItemImpl() const;
+  bool IsRestoreSessionInProgress() const;
 
   // Implementation for corresponding NavigationManager getters.
   NavigationItemImpl* GetPendingItemImpl() const;
diff --git a/ios/web/navigation/navigation_manager_impl.mm b/ios/web/navigation/navigation_manager_impl.mm
index 1aa8b484..4719013 100644
--- a/ios/web/navigation/navigation_manager_impl.mm
+++ b/ios/web/navigation/navigation_manager_impl.mm
@@ -847,6 +847,10 @@
   return GetLastCommittedItemImpl();
 }
 
+bool NavigationManagerImpl::IsRestoreSessionInProgress() const {
+  return native_restore_in_progress_;
+}
+
 NavigationItemImpl* NavigationManagerImpl::GetLastCommittedItemImpl() const {
   if (empty_window_open_item_) {
     return empty_window_open_item_.get();
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 0cc4a777..34f234f 100644
--- a/ios/web/web_state/ui/crw_web_controller_unittest.mm
+++ b/ios/web/web_state/ui/crw_web_controller_unittest.mm
@@ -9,6 +9,7 @@
 #import <memory>
 #import <utility>
 
+#import "base/apple/bundle_locations.h"
 #import "base/apple/foundation_util.h"
 #import "base/scoped_observation.h"
 #import "base/strings/sys_string_conversions.h"
@@ -22,6 +23,7 @@
 #import "ios/web/common/uikit_ui_util.h"
 #import "ios/web/js_messaging/web_view_js_utils.h"
 #import "ios/web/navigation/block_universal_links_buildflags.h"
+#import "ios/web/navigation/crw_wk_navigation_handler.h"
 #import "ios/web/navigation/crw_wk_navigation_states.h"
 #import "ios/web/navigation/navigation_item_impl.h"
 #import "ios/web/navigation/navigation_manager_impl.h"
@@ -62,6 +64,27 @@
 #import "third_party/ocmock/ocmock_extensions.h"
 #import "url/scheme_host_port.h"
 
+@interface CRWWebController (Testing)
+@property(nonatomic, readonly) CRWWKNavigationHandler* navigationHandler;
+@end
+
+@interface CRWWKNavigationHandler (Testing)
+@property(nonatomic, copy) NSURL* allowedErrorPageFileURL;
+@end
+@interface FakeWKFrameInfo : NSObject <NSCopying>
+@property(nonatomic, assign, getter=isMainFrame) BOOL mainFrame;
+@end
+
+@implementation FakeWKFrameInfo
+@synthesize mainFrame = _mainFrame;
+
+- (id)copyWithZone:(NSZone*)zone {
+  FakeWKFrameInfo* copy = [[[self class] allocWithZone:zone] init];
+  copy.mainFrame = self.mainFrame;
+  return copy;
+}
+@end
+
 using base::test::ios::kWaitForJSCompletionTimeout;
 using base::test::ios::kWaitForPageLoadTimeout;
 using base::test::ios::WaitUntilConditionOrTimeout;
@@ -1248,6 +1271,85 @@
       url_request, WKNavigationActionPolicyCancel));
 }
 
+// Tests that a forged navigation to an error page URL is blocked/cancelled
+// when the browser didn't initiate it, and successfully allowed when the
+// browser did, but restricted strictly to main frame navigations and cleared.
+TEST_F(CRWWebControllerPolicyDeciderTest, RejectForgedErrorPageNavigation) {
+  NSString* path =
+      [base::apple::FrameworkBundle() pathForResource:@"error_page_loaded"
+                                               ofType:@"html"];
+  ASSERT_TRUE(path);
+  NSURL* forged_error_url = [NSURL
+      URLWithString:[NSString
+                        stringWithFormat:@"file://%@?url=chrome://settings",
+                                         path]];
+  NSMutableURLRequest* forged_request =
+      [NSMutableURLRequest requestWithURL:forged_error_url];
+  forged_request.mainDocumentURL = forged_error_url;
+
+  // Use FakeWKFrameInfo to avoid OCMock copying and swizzling issues.
+  FakeWKFrameInfo* fake_frame = [[FakeWKFrameInfo alloc] init];
+
+  CRWFakeWKNavigationAction* action = [[CRWFakeWKNavigationAction alloc] init];
+  action.request = forged_request;
+
+  // Default case: error page load is blocked when not initiated by browser.
+  fake_frame.mainFrame = YES;
+  action.targetFrame = (WKFrameInfo*)fake_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;
+  }));
+
+  [web_controller() navigationHandler].allowedErrorPageFileURL =
+      forged_error_url;
+
+  // Subframe error page load is blocked and does not clear the flag.
+  fake_frame.mainFrame = NO;
+  action.targetFrame = (WKFrameInfo*)fake_frame;
+  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;
+  }));
+
+  EXPECT_TRUE([[web_controller() navigationHandler].allowedErrorPageFileURL
+      isEqual:forged_error_url]);
+
+  // Main frame navigation is allowed and clears the flag.
+  fake_frame.mainFrame = YES;
+  action.targetFrame = (WKFrameInfo*)fake_frame;
+  callback_called = false;
+  [navigation_delegate_ webView:mock_web_view_
+      decidePolicyForNavigationAction:action
+                          preferences:[[WKWebpagePreferences alloc] init]
+                      decisionHandler:^(WKNavigationActionPolicy policy,
+                                        WKWebpagePreferences* ignored) {
+                        EXPECT_EQ(policy, WKNavigationActionPolicyAllow);
+                        callback_called = true;
+                      }];
+  EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^{
+    return callback_called;
+  }));
+
+  EXPECT_FALSE([web_controller() navigationHandler].allowedErrorPageFileURL);
+}
+
 // Test fixture for window.open tests.
 class WindowOpenByDomTest : public WebTestWithWebController {
  protected:
Loading diff…

Regression Test / PoC

shipped with the fix
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 0cc4a777..34f234f 100644
--- a/ios/web/web_state/ui/crw_web_controller_unittest.mm
+++ b/ios/web/web_state/ui/crw_web_controller_unittest.mm
@@ -9,6 +9,7 @@
 #import <memory>
 #import <utility>
 
+#import "base/apple/bundle_locations.h"
 #import "base/apple/foundation_util.h"
 #import "base/scoped_observation.h"
 #import "base/strings/sys_string_conversions.h"
@@ -22,6 +23,7 @@
 #import "ios/web/common/uikit_ui_util.h"
 #import "ios/web/js_messaging/web_view_js_utils.h"
 #import "ios/web/navigation/block_universal_links_buildflags.h"
+#import "ios/web/navigation/crw_wk_navigation_handler.h"
 #import "ios/web/navigation/crw_wk_navigation_states.h"
 #import "ios/web/navigation/navigation_item_impl.h"
 #import "ios/web/navigation/navigation_manager_impl.h"
@@ -62,6 +64,27 @@
 #import "third_party/ocmock/ocmock_extensions.h"
 #import "url/scheme_host_port.h"
 
+@interface CRWWebController (Testing)
+@property(nonatomic, readonly) CRWWKNavigationHandler* navigationHandler;
+@end
+
+@interface CRWWKNavigationHandler (Testing)
+@property(nonatomic, copy) NSURL* allowedErrorPageFileURL;
+@end
+@interface FakeWKFrameInfo : NSObject <NSCopying>
+@property(nonatomic, assign, getter=isMainFrame) BOOL mainFrame;
+@end
+
+@implementation FakeWKFrameInfo
+@synthesize mainFrame = _mainFrame;
+
+- (id)copyWithZone:(NSZone*)zone {
+  FakeWKFrameInfo* copy = [[[self class] allocWithZone:zone] init];
+  copy.mainFrame = self.mainFrame;
+  return copy;
+}
+@end
+
 using base::test::ios::kWaitForJSCompletionTimeout;
 using base::test::ios::kWaitForPageLoadTimeout;
 using base::test::ios::WaitUntilConditionOrTimeout;
@@ -1248,6 +1271,85 @@
       url_request, WKNavigationActionPolicyCancel));
 }
 
+// Tests that a forged navigation to an error page URL is blocked/cancelled
+// when the browser didn't initiate it, and successfully allowed when the
+// browser did, but restricted strictly to main frame navigations and cleared.
+TEST_F(CRWWebControllerPolicyDeciderTest, RejectForgedErrorPageNavigation) {
+  NSString* path =
+      [base::apple::FrameworkBundle() pathForResource:@"error_page_loaded"
+                                               ofType:@"html"];
+  ASSERT_TRUE(path);
+  NSURL* forged_error_url = [NSURL
+      URLWithString:[NSString
+                        stringWithFormat:@"file://%@?url=chrome://settings",
+                                         path]];
+  NSMutableURLRequest* forged_request =
+      [NSMutableURLRequest requestWithURL:forged_error_url];
+  forged_request.mainDocumentURL = forged_error_url;
+
+  // Use FakeWKFrameInfo to avoid OCMock copying and swizzling issues.
+  FakeWKFrameInfo* fake_frame = [[FakeWKFrameInfo alloc] init];
+
+  CRWFakeWKNavigationAction* action = [[CRWFakeWKNavigationAction alloc] init];
+  action.request = forged_request;
+
+  // Default case: error page load is blocked when not initiated by browser.
+  fake_frame.mainFrame = YES;
+  action.targetFrame = (WKFrameInfo*)fake_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;
+  }));
+
+  [web_controller() navigationHandler].allowedErrorPageFileURL =
+      forged_error_url;
+
+  // Subframe error page load is blocked and does not clear the flag.
+  fake_frame.mainFrame = NO;
+  action.targetFrame = (WKFrameInfo*)fake_frame;
+  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;
+  }));
+
+  EXPECT_TRUE([[web_controller() navigationHandler].allowedErrorPageFileURL
+      isEqual:forged_error_url]);
+
+  // Main frame navigation is allowed and clears the flag.
+  fake_frame.mainFrame = YES;
+  action.targetFrame = (WKFrameInfo*)fake_frame;
+  callback_called = false;
+  [navigation_delegate_ webView:mock_web_view_
+      decidePolicyForNavigationAction:action
+                          preferences:[[WKWebpagePreferences alloc] init]
+                      decisionHandler:^(WKNavigationActionPolicy policy,
+                                        WKWebpagePreferences* ignored) {
+                        EXPECT_EQ(policy, WKNavigationActionPolicyAllow);
+                        callback_called = true;
+                      }];
+  EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^{
+    return callback_called;
+  }));
+
+  EXPECT_FALSE([web_controller() navigationHandler].allowedErrorPageFileURL);
+}
+
 // Test fixture for window.open tests.
 class WindowOpenByDomTest : public WebTestWithWebController {
  protected:
Loading diff…

Original Bug Report

reported by li...@chromium.org

Potential WebUI Privilege Escalation via Forged Error Page Navigation in iOS

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 compromised renderer process on iOS can potentially escalate its privileges to a WebUI context. It achieves this by forging a navigation to a local error page file, which bypasses validation and subsequently authorizes a redirect to a privileged chrome:// URL.

Affected files:

  • ios/web/navigation/crw_wk_navigation_handler.mm
  • ios/web/navigation/crw_error_page_helper.mm

Estimated timestamp from git blame: 2025-06-25

Description

A potential Confused Deputy vulnerability exists in the iOS Chromium navigation handler (crw_wk_navigation_handler.mm). It may allow a compromised renderer process (Renderer RCE) to bypass navigation restrictions and force the browser to load and grant privileges to App-Specific URLs (e.g., chrome:// WebUI pages).

The Flaw

The vulnerability stems from insufficient validation of navigations to internal error pages. In ios/web/navigation/crw_wk_navigation_handler.mm, the webView:decidePolicyForNavigationAction:preferences:decisionHandler: method unconditionally allows a navigation if the destination URL is identified as an internal error page:

// If this is a error navigation, pass through.
if ([CRWErrorPageHelper isErrorPageFileURL:requestURL]) {
  decisionHandler(WKNavigationActionPolicyAllow);
  return;
}

The helper method isErrorPageFileURL: (in ios/web/navigation/crw_error_page_helper.mm) only verifies that the URL scheme is file://, the path matches the expected app bundle path for error_page_loaded.html, and it contains a valid url parameter. It lacks an unforgeable token or state verification to distinguish between a legitimate browser-initiated error page load and a malicious renderer-initiated load.

Potential Exploitation Chain

(Note: These are potential steps based on code analysis; a functional exploit has not been executed).

  1. Renderer Compromise: An attacker achieves Remote Code Execution (RCE) in the sandboxed renderer process (WebContent on iOS).
  2. Path Discovery: The compromised renderer identifies its own iOS app bundle path to construct the exact local file path to error_page_loaded.html.
  3. Forged Navigation: The attacker executes JavaScript in the compromised renderer to initiate a main-frame navigation to the local error page file, bypassing standard web restrictions by appending a target URL: window.location.href = "file://[bundle_path]/error_page_loaded.html?url=chrome://settings".
  4. Policy Bypass: The Browser process receives this request in decidePolicyForNavigationAction:. The isErrorPageFileURL: check returns YES, and the navigation is allowed early, bypassing further security checks.
  5. State Corruption: When the navigation commits, the Browser process updates its internal tracking of the current page URL (self.documentURL) to this malicious file URL.
  6. Error Page Execution: The legitimate error_page_loaded.html file loads in the renderer. Its embedded JavaScript automatically executes window.location.replace(failedUrl), where failedUrl is the chrome://settings parameter.
  7. WebUI Escalation: The renderer initiates a second navigation to chrome://settings. The Browser process evaluates this using shouldAllowAppSpecificURLNavigationAction:. Because the current documentURL is now an error page, the following check passes:
// Allow navigation to WebUI pages from error pages.
if ([CRWErrorPageHelper isErrorPageFileURL:self.documentURL]) {
  return YES;
}
  1. Sandbox Escape: The Browser process allows the navigation and proceeds to call createWebUIForURL:, which provisions native WebUI bindings to the renderer. The attacker can now send highly privileged IPC messages, resulting in a full Sandbox Escape.

Suggested Fix

Implement an unforgeable, cryptographically secure token mechanism for error pages. When the Browser process legitimately initiates an error page navigation, it should generate a random token, append it to the URL, and store it in the NavigationContext. The isErrorPageFileURL: (or a related check during policy decision) must verify that the token in the URL matches a valid, recently issued token by the Browser. Alternatively, strict checks should be enforced to ensure that file:// navigations to internal error pages can only occur if they are explicitly marked as Browser-initiated.

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