Medium chrome Authorization Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMissing authorization in Navigation
DescriptionMissing authorization in Navigation
ComponentNavigation
Sub-componentiOS
Bug ClassAuthorization Error
Tracker504215649
Fix commit8e5313fe4b07 (chromium/src) +89/-15
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-06

Changed Functions

FunctionChangeNotes
if
ios/web/navigation/crw_error_page_helper.mm
modified
TEST_F
ios/web/navigation/crw_error_page_helper_unittest.mm
modified

Files Changed

  • ios/web/navigation/crw_error_page_helper.h
  • ios/web/web_state/ui/crw_web_request_controller.mm
  • ios/components/security_interstitials/ios_security_interstitial_java_script_feature.mm
  • ios/web/navigation/crw_error_page_helper.mm
  • ios/web/web_state/error_page_inttest.mm
  • ios/web/navigation/crw_wk_navigation_handler.mm
  • ios/web/navigation/crw_error_page_helper_unittest.mm
From 8e5313fe4b0716d6a4e9db0fe6977a73d5120c9c Mon Sep 17 00:00:00 2001
From: Hamza Mohammed <[email protected]>
Date: Fri, 11 Sep 2026 13:00:00 -0700
Subject: [PATCH] [iOS] Commit error pages with file URL origin on reload

Previously, when presenting an error page on a reload or matching
history entry, `CRWWKNavigationHandler` loaded an empty HTML string
using `errorPage.failedNavigationURL` as `baseURL`. This caused the
error page document to inherit the web origin of the blocked site,
allowing a same-origin opener window to access the interstitial DOM and
invoke `IOSInterstitialMessage` commands (e.g., `CMD_PROCEED`).

To fix this:
- Add `errorPageFileURLWithoutDontLoad` to `CRWErrorPageHelper` to
  construct the error page file URL without the `dontLoad` query
  parameter.
- Pass `errorPage.errorPageFileURLWithoutDontLoad` as `baseURL` and
  `allowedErrorPageFileURL` in `displayErrorPageWithError:` when
  presenting the placeholder HTML string. This ensures the document
  commits under a `file:` origin while omitting `dontLoad`, so restored
  tabs will immediately reload the failed navigation URL without
  requiring `history.replaceState` in injected scripts.
- Fall back to `loadCurrentURLWithRendererInitiatedNavigation:` in
  `CRWWebRequestController` when reloading an error page `file:` URL
  so the underlying failed `NavigationItem` URL is re-requested rather
  than reloading the empty placeholder string in WKWebView.
- Restrict `IOSSecurityInterstitialJavaScriptFeature` to only accept
  messages originating from `file:` URLs as defense-in-depth.
- Add unit test coverage in `CRWErrorPageHelperTest`.

Fixed: 504215649
Change-Id: I6c6b3b135f16b4802482955d2cc4eb7919a9c963
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8373266
Reviewed-by: Daniel White <[email protected]>
Commit-Queue: Hamza Mohammed <[email protected]>
Reviewed-by: Rohit Rao <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1696524}
---
diff --git a/ios/components/security_interstitials/ios_security_interstitial_java_script_feature.mm b/ios/components/security_interstitials/ios_security_interstitial_java_script_feature.mm
index 3348b80..c3d2cf8 100644
--- a/ios/components/security_interstitials/ios_security_interstitial_java_script_feature.mm
+++ b/ios/components/security_interstitials/ios_security_interstitial_java_script_feature.mm
@@ -45,6 +45,13 @@
     return;
   }
 
+  // Security interstitials are committed at local file:// URLs. Ignore
+  // messages sent from web origins (e.g., http:// or https://).
+  if (!script_message.request_url() ||
+      !script_message.request_url()->SchemeIsFile()) {
+    return;
+  }
+
   if (!script_message.legacy_body() ||
       !script_message.legacy_body()->is_dict()) {
     return;
diff --git a/ios/web/navigation/crw_error_page_helper.h b/ios/web/navigation/crw_error_page_helper.h
index f0389fe1..7df2264 100644
--- a/ios/web/navigation/crw_error_page_helper.h
+++ b/ios/web/navigation/crw_error_page_helper.h
@@ -17,6 +17,9 @@
 @property(nonatomic, strong, readonly) NSURL* failedNavigationURL;
 // The error page file to be loaded as a new page.
 @property(nonatomic, strong, readonly) NSURL* errorPageFileURL;
+// The error page file URL without the 'dontLoad' query parameter, used as the
+// baseURL when presenting an error page via `loadHTMLString:baseURL:`.
+@property(nonatomic, strong, readonly) NSURL* errorPageFileURLWithoutDontLoad;
 
 - (instancetype)initWithError:(NSError*)error NS_DESIGNATED_INITIALIZER;
 
diff --git a/ios/web/navigation/crw_error_page_helper.mm b/ios/web/navigation/crw_error_page_helper.mm
index d943d8be8..fef97e1b 100644
--- a/ios/web/navigation/crw_error_page_helper.mm
+++ b/ios/web/navigation/crw_error_page_helper.mm
@@ -54,6 +54,7 @@
 
 @synthesize failedNavigationURL = _failedNavigationURL;
 @synthesize errorPageFileURL = _errorPageFileURL;
+@synthesize errorPageFileURLWithoutDontLoad = _errorPageFileURLWithoutDontLoad;
 
 - (instancetype)initWithError:(NSError*)error {
   if ((self = [super init])) {
@@ -71,23 +72,39 @@
   return _failedNavigationURL;
 }
 
-- (NSURL*)errorPageFileURL {
-  if (!_errorPageFileURL) {
-    NSURLQueryItem* itemURL = [NSURLQueryItem
-        queryItemWithName:base::SysUTF8ToNSString(kOriginalUrlKey)
-                    value:EscapeHTMLCharacters(
-                              self.failedNavigationURL.absoluteString)];
+- (NSURL*)errorPageFileURLWithDontLoad:(BOOL)dontLoad {
+  NSURLQueryItem* itemURL = [NSURLQueryItem
+      queryItemWithName:base::SysUTF8ToNSString(kOriginalUrlKey)
+                  value:EscapeHTMLCharacters(
+                            self.failedNavigationURL.absoluteString)];
+  NSURLComponents* components =
+      [[NSURLComponents alloc] initWithString:@"file:///"];
+  components.path = LoadedErrorPageFilePath();
+  if (dontLoad) {
     NSURLQueryItem* itemDontLoad = [NSURLQueryItem queryItemWithName:@"dontLoad"
                                                                value:@"true"];
-    NSURLComponents* URL = [[NSURLComponents alloc] initWithString:@"file:///"];
-    URL.path = LoadedErrorPageFilePath();
-    URL.queryItems = @[ itemURL, itemDontLoad ];
-    DCHECK(URL.URL) << "file URL should be valid";
-    _errorPageFileURL = URL.URL;
+    components.queryItems = @[ itemURL, itemDontLoad ];
+  } else {
+    components.queryItems = @[ itemURL ];
+  }
+  DCHECK(components.URL) << "file URL should be valid";
+  return components.URL;
+}
+
+- (NSURL*)errorPageFileURL {
+  if (!_errorPageFileURL) {
+    _errorPageFileURL = [self errorPageFileURLWithDontLoad:YES];
   }
   return _errorPageFileURL;
 }
 
+- (NSURL*)errorPageFileURLWithoutDontLoad {
+  if (!_errorPageFileURLWithoutDontLoad) {
+    _errorPageFileURLWithoutDontLoad = [self errorPageFileURLWithDontLoad:NO];
+  }
+  return _errorPageFileURLWithoutDontLoad;
+}
+
 - (NSString*)automaticReloadJavaScript {
   if (!_automaticReloadJavaScript) {
     NSString* path = InjectedErrorPageFilePath();
diff --git a/ios/web/navigation/crw_wk_navigation_handler.mm b/ios/web/navigation/crw_wk_navigation_handler.mm
index 04ccdbba..b113566 100644
--- a/ios/web/navigation/crw_wk_navigation_handler.mm
+++ b/ios/web/navigation/crw_wk_navigation_handler.mm
@@ -2211,8 +2211,21 @@
     errorNavigation = [webView loadFileURL:errorPage.errorPageFileURL
                    allowingReadAccessToURL:errorPage.errorPageFileURL];
   } else {
-    errorNavigation = [webView loadHTMLString:@""
-                                      baseURL:errorPage.failedNavigationURL];
+    // SECURITY: Use the error page file URL as the `baseURL` so the committed
+    // error page document gets a `file:` origin instead of the web origin of
+    // the failed URL. Committing the document at `failedNavigationURL` lets a
+    // same-origin window (e.g. an opener) script the interstitial document,
+    // intercept/suppress the injected warning HTML, and post
+    // `IOSInterstitialMessage` commands (e.g. `CMD_PROCEED`) with no user
+    // interaction.
+    // Use `errorPageFileURLWithoutDontLoad` without `dontLoad` so that if this
+    // error page is later restored from session history,
+    // `error_page_loaded.html` will immediately reload the failed navigation
+    // URL.
+    self.allowedErrorPageFileURL = errorPage.errorPageFileURLWithoutDontLoad;
+    errorNavigation =
+        [webView loadHTMLString:@""
+                        baseURL:errorPage.errorPageFileURLWithoutDontLoad];
   }
   [self.navigationStates setState:web::WKNavigationState::REQUESTED
                     forNavigation:errorNavigation];
diff --git a/ios/web/web_state/error_page_inttest.mm b/ios/web/web_state/error_page_inttest.mm
index 937cc984..f739ca8 100644
--- a/ios/web/web_state/error_page_inttest.mm
+++ b/ios/web/web_state/error_page_inttest.mm
@@ -243,6 +243,8 @@
   ASSERT_TRUE(security_state_info()->visible_ssl_status);
   EXPECT_EQ(SECURITY_STYLE_UNAUTHENTICATED,
             security_state_info()->visible_ssl_status->security_style);
+  EXPECT_TRUE([@"file:"
+      isEqualToString:ExecuteJavaScript(@"window.location.protocol")]);
 }
 
 // Sucessfully loads the page, goes back, stops the server, goes forward and
diff --git a/ios/web/web_state/ui/crw_web_request_controller.mm b/ios/web/web_state/ui/crw_web_request_controller.mm
index 2b3121d..e47bb34 100644
--- a/ios/web/web_state/ui/crw_web_request_controller.mm
+++ b/ios/web/web_state/ui/crw_web_request_controller.mm
@@ -14,6 +14,7 @@
 #import "base/strings/sys_string_conversions.h"
 #import "ios/web/common/features.h"
 #import "ios/web/common/referrer_util.h"
+#import "ios/web/navigation/crw_error_page_helper.h"
 #import "ios/web/navigation/crw_navigation_item_holder.h"
 #import "ios/web/navigation/crw_pending_navigation_info.h"
 #import "ios/web/navigation/crw_wk_navigation_handler.h"
@@ -183,8 +184,15 @@
 
   self.currentNavItem->SetTransitionType(
       ui::PageTransition::PAGE_TRANSITION_RELOAD);
-  if (!web::GetWebClient()->IsAppSpecificURL(
-          net::GURLWithNSURL(self.webView.URL))) {
+  GURL webViewURL = net::GURLWithNSURL(self.webView.URL);
+  // Error pages loaded via `loadHTMLString:baseURL:` with an error page `file:`
+  // URL cannot be reloaded directly via `[self.webView reload]`, as WebKit
+  // would reload the empty placeholder string rather than re-requesting the
+  // failed navigation URL. Fall back to
+  // `loadCurrentURLWithRendererInitiatedNavigation:` to reload the underlying
+  // `NavigationItem` URL.
+  if (!web::GetWebClient()->IsAppSpecificURL(webViewURL) &&
+      ![CRWErrorPageHelper isErrorPageFileURL:webViewURL]) {
     // New navigation manager can delegate directly to WKWebView to reload
     // for non-app-specific URLs. The necessary navigation states will be
     // updated in WKNavigationDelegate callbacks.
diff --git a/ios/web/navigation/crw_error_page_helper_unittest.mm b/ios/web/navigation/crw_error_page_helper_unittest.mm
index 3200be2..a1693f06 100644
--- a/ios/web/navigation/crw_error_page_helper_unittest.mm
+++ b/ios/web/navigation/crw_error_page_helper_unittest.mm
@@ -7,6 +7,7 @@
 #import "base/apple/bundle_locations.h"
 #import "base/strings/sys_string_conversions.h"
 #import "net/base/apple/url_conversions.h"
+#import "net/base/url_util.h"
 #import "testing/gtest/include/gtest/gtest.h"
 #import "testing/gtest_mac.h"
 #import "testing/platform_test.h"
@@ -46,6 +47,29 @@
   EXPECT_TRUE([CRWErrorPageHelper isErrorPageFileURL:url_from_helper]);
 }
 
+// Tests that errorPageFileURLWithoutDontLoad does not contain the 'dontLoad'
+// parameter but still contains the failed URL and is identified as an error
+// page.
+TEST_F(CRWErrorPageHelperTest, ErrorPageFileURLWithoutDontLoad) {
+  NSString* url_string = @"https://test-error-page.com";
+  NSError* error = [NSError
+      errorWithDomain:NSURLErrorDomain
+                 code:NSURLErrorBadURL
+             userInfo:@{
+               NSURLErrorFailingURLErrorKey : [NSURL URLWithString:url_string]
+             }];
+  CRWErrorPageHelper* helper = [[CRWErrorPageHelper alloc] initWithError:error];
+  GURL url = net::GURLWithNSURL(helper.errorPageFileURLWithoutDontLoad);
+  EXPECT_TRUE([CRWErrorPageHelper isErrorPageFileURL:url]);
+  EXPECT_EQ(GURL(base::SysNSStringToUTF8(url_string)),
+            [CRWErrorPageHelper failedNavigationURLFromErrorPageFileURL:url]);
+  EXPECT_TRUE([helper isErrorPageFileURLForFailedNavigationURL:
+                          helper.errorPageFileURLWithoutDontLoad]);
+
+  std::string dont_load_value;
+  EXPECT_FALSE(net::GetValueForKeyInQuery(url, "dontLoad", &dont_load_value));
+}
+
 // Tests that the error page is correctly identified as error page.
 TEST_F(CRWErrorPageHelperTest, IsErrorPageFileURL) {
   NSString* url_string = @"https://test-error-page.com";
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/ios/web/navigation/crw_error_page_helper_unittest.mm b/ios/web/navigation/crw_error_page_helper_unittest.mm
index 3200be2..a1693f06 100644
--- a/ios/web/navigation/crw_error_page_helper_unittest.mm
+++ b/ios/web/navigation/crw_error_page_helper_unittest.mm
@@ -7,6 +7,7 @@
 #import "base/apple/bundle_locations.h"
 #import "base/strings/sys_string_conversions.h"
 #import "net/base/apple/url_conversions.h"
+#import "net/base/url_util.h"
 #import "testing/gtest/include/gtest/gtest.h"
 #import "testing/gtest_mac.h"
 #import "testing/platform_test.h"
@@ -46,6 +47,29 @@
   EXPECT_TRUE([CRWErrorPageHelper isErrorPageFileURL:url_from_helper]);
 }
 
+// Tests that errorPageFileURLWithoutDontLoad does not contain the 'dontLoad'
+// parameter but still contains the failed URL and is identified as an error
+// page.
+TEST_F(CRWErrorPageHelperTest, ErrorPageFileURLWithoutDontLoad) {
+  NSString* url_string = @"https://test-error-page.com";
+  NSError* error = [NSError
+      errorWithDomain:NSURLErrorDomain
+                 code:NSURLErrorBadURL
+             userInfo:@{
+               NSURLErrorFailingURLErrorKey : [NSURL URLWithString:url_string]
+             }];
+  CRWErrorPageHelper* helper = [[CRWErrorPageHelper alloc] initWithError:error];
+  GURL url = net::GURLWithNSURL(helper.errorPageFileURLWithoutDontLoad);
+  EXPECT_TRUE([CRWErrorPageHelper isErrorPageFileURL:url]);
+  EXPECT_EQ(GURL(base::SysNSStringToUTF8(url_string)),
+            [CRWErrorPageHelper failedNavigationURLFromErrorPageFileURL:url]);
+  EXPECT_TRUE([helper isErrorPageFileURLForFailedNavigationURL:
+                          helper.errorPageFileURLWithoutDontLoad]);
+
+  std::string dont_load_value;
+  EXPECT_FALSE(net::GetValueForKeyInQuery(url, "dontLoad", &dont_load_value));
+}
+
 // Tests that the error page is correctly identified as error page.
 TEST_F(CRWErrorPageHelperTest, IsErrorPageFileURL) {
   NSString* url_string = @"https://test-error-page.com";
Loading diff…

Original Bug Report

reported by [email protected]

Security Interstitial Bypass on iOS via history.replaceState

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: In Chrome on iOS, security error pages (e.g., Safe Browsing) can be forced to commit with the blocked site’s origin rather than a safe local file URL. By using history.replaceState in a same-origin popup before triggering a block, an attacker can retain DOM access to the interstitial. This allows them to intercept the warning UI and programmatically send a proceed command, silently bypassing the security restriction.

Affected files:

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

Estimated timestamp from git blame: 2024-10-02

Description

When Chrome for iOS encounters a navigation blocked by security features like Safe Browsing, SSL errors, or Lookalike URLs, it displays an interstitial error page. This is managed by -[CRWWKNavigationHandler displayErrorPageWithError:inWebView:isProvisionalLoad:].

During this process, the handler evaluates whether to load a local error file:// URL or to inject the error page using loadHTMLString:baseURL:. If the WKWebView’s current backForwardList.currentItem.URL already matches the blocked URL, the handler assumes it needs to replace the current page and falls back to [webView loadHTMLString:@"" baseURL:errorPage.failedNavigationURL].

An attacker can reliably trigger this fallback by opening a same-origin popup and using history.replaceState to change the popup’s URL to the blocked URL before calling location.reload(). Because the baseURL for the interstitial is set to the blocked URL, the resulting document inherits that origin.

If the attacker’s “opener” window shares this origin, it retains full synchronous DOM access to the interstitial document. The opener can then suppress the warning UI (e.g., by overriding document.write before the interstitial HTML is injected) and silently send a CMD_PROCEED (command ID 1) via the IOSInterstitialMessage WebKit message handler. The browser interprets this as deliberate user consent, allowing the unsafe navigation to proceed without the user ever seeing the warning.

Potential Steps to Reproduce

(Note: These are suggested steps based on static code analysis; our tooling agent has not executed a live proof-of-concept.)

  1. An attacker hosts a page at https://a.example/opener.
  2. The attacker ensures another URL on the same origin, e.g., https://a.example/flagged, triggers a security interstitial (such as being on the Safe Browsing blocklist).
  3. A victim visits https://a.example/opener.
  4. The attacker’s script opens a popup: w = window.open('https://a.example/ok').
  5. Once loaded, the script alters the popup’s history state: w.history.replaceState(null, '', '/flagged').
  6. The script triggers the blocked navigation: w.location.reload().
  7. Chrome intercepts the navigation, cancels it, and prepares the error page. Because the history item URL matches the failed URL, it takes the loadHTMLString:baseURL: path.
  8. The attacker’s script immediately hooks the document to suppress the UI: w.document.write = function() {}.
  9. The script programmatically dismisses the warning: w.webkit.messageHandlers.IOSInterstitialMessage.postMessage({command: '1'}).
  10. The browser registers the bypass and navigates the victim to the dangerous payload.

Suggested Fix

Using loadHTMLString:baseURL: with the failedNavigationURL grants the resulting document the security context of the blocked site, breaking the isolation of the interstitial.

  1. Use Opaque/Safe Origins: Error pages should be committed in an isolated context, ideally always using a file:// URL or an opaque origin, mimicking desktop Chrome’s behavior. If loadHTMLString: must be used, the baseURL should not be the potentially attacker-controlled failed URL.
  2. Message Authentication: IOSSecurityInterstitialJavaScriptFeature::ScriptMessageReceived could implement stricter checks to ensure that messages like CMD_PROCEED are originating from a definitively trusted interstitial context, rather than a document sharing an origin with web content.

Evaluated with Chrome root at commit: 7353d249d9cacf9c7218e1d7b8a39cf39c72d646


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