Medium watch CVSS 5.4 chrome Spoofing 🔧 Commit mapped

Overview

Medium
Severity
5.4
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUI misrepresentation in Chromium
DescriptionUI misrepresentation in Chromium
ComponentChromium
Sub-componentiOS
Bug ClassSpoofing
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.
Tracker546438368
Fix commit25718a2bb2bc (chromium/src) +97/-18
CWECWE-451
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:L
CISA KEVNot listed
CreditedWihdatu Nuuro Ahmadi
Reported2026-08-14 (per vendor advisory)
Fix landed2026-08-27 (public git)
Disclosed2026-09-22
Patch gap26 days — fix public in source before the advisory

Background

`CRWWKNavigationHandler`
the iOS //ios/web Objective-C++ class that bridges WKWebView navigation callbacks into Chromium’s navigation stack.
Provisional server redirect
a 3xx redirect received while a navigation is still provisional (not yet committed), delivered via WKNavigationDelegate’s didReceiveServerRedirectForProvisionalNavigation:.
`NavigationItemImpl` / `GetVisibleItem()`
NavigationItemImpl holds a navigation’s URL and virtual URL, and NavigationManager::GetVisibleItem() returns the browser-initiated pending item (used to paint the omnibox) while a page loads.
`NavigationContextImpl`
the per-navigation object that tracks in-flight state such as the current (possibly redirected) URL via GetUrl() and commit status via SetHasCommitted().

Root Cause Analysis

When CRWWKNavigationHandler received a provisional server redirect, it immediately called item->SetURL(webViewURL) and item->SetVirtualURL(webViewURL) on the pending NavigationItemImpl. Because browser-initiated pending items are returned by GetVisibleItem() during loading, the omnibox would render the redirect destination’s domain and its secure padlock before the destination document had committed, violating the invariant that the visible item reflects the currently painted document. If the destination stalled or failed to commit, the original page stayed painted while the omnibox showed the spoofed destination URL.

The fix stops mutating the pending item’s URL/virtual URL on redirect, instead tracking the redirected URL only on NavigationContextImpl, and defers applying that URL to the navigation item until commit time in webPageChangedWithContext:webView: and didCommitNavigation:, so GetVisibleItem() keeps returning the original requested URL until the navigation actually commits.

Key insight
The core mistake was updating the visible (pending) navigation item’s URL at provisional-redirect time rather than at commit time, letting the address bar advertise a destination the browser had not yet loaded. The fix keeps the requested URL on the pending item and only writes the redirected context->GetUrl() into the item when it is committed.

Attack Path

  1. Lure to attacker page The victim navigates to an attacker-controlled URL that the attacker can cause to issue a server-side redirect toward a spoofed target domain.
  2. Trigger provisional redirect The server responds with a 3xx redirect, invoking didReceiveServerRedirectForProvisionalNavigation:, which previously set the pending NavigationItemImpl URL and virtual URL to the destination webViewURL.
  3. Omnibox reflects uncommitted destination GetVisibleItem() returns that pending item, so the omnibox displays the destination domain and secure padlock while the destination document has not committed.
  4. Stall the commit The attacker ensures the destination never commits (stall or fail to load), so the originating attacker document stays painted underneath the spoofed omnibox URL. (inferred)
  5. Present spoofed content The user sees a trusted-looking address and padlock over attacker-controlled page content, enabling phishing or trust deception. (inferred)

Impact Assessment

An attacker gains address bar spoofing on iOS Chromium: the omnibox can show a spoofed destination domain and its padlock while attacker-controlled content remains painted, undermining the origin the user believes they are viewing. This is a UI-misrepresentation logic error in the browser process navigation code and requires the victim to visit an attacker-controlled page that issues a provisional server redirect that then stalls or fails to commit. The advisory classifies this as UI misrepresentation with no stated exploited-in-the-wild status, and any escalation beyond visual spoofing is not demonstrated by the patch. (inferred)

Changed Functions

FunctionChangeNotes
if
ios/web/navigation/crw_wk_navigation_handler.mm
modified
RedirectVisibleItemObserver
ios/web/navigation/crw_wk_navigation_handler_inttest.mm
modified
TEST_F
ios/web/navigation/crw_wk_navigation_handler_inttest.mm
modified
ACTION_P
ios/web/web_state/web_state_observer_inttest.mm
modified

Files Changed

  • ios/web/navigation/crw_wk_navigation_handler_inttest.mm
  • ios/web/web_state/web_state_observer_inttest.mm
  • ios/web/navigation/crw_wk_navigation_handler.mm

Audit Directions

  • Pending-item mutation before commit
    Look for other paths that write URL, virtual URL, or security state into pending/visible NavigationItemImpl objects during provisional or redirect callbacks instead of at commit.
  • Visible-vs-committed divergence
    Audit GetVisibleItem() consumers (omnibox, padlock/security indicators) across platforms for cases where the visible item can reflect a destination whose document is not yet painted.
  • Redirect and commit handling
    Review NavigationContextImpl::GetUrl() versus NavigationItemImpl URL synchronization in redirect, cancellation, and stalled-commit flows to ensure the address bar never leads the committed document.
From 25718a2bb2bcdfeabb72c167efd2fcb133a12173 Mon Sep 17 00:00:00 2001
From: Mike Dougherty <[email protected]>
Date: Thu, 27 Aug 2026 09:59:22 -0700
Subject: [PATCH] [iOS] Prevent address bar spoofing on provisional server redirects

Previously, `CRWWKNavigationHandler` updated the pending
`NavigationItemImpl`'s URL and virtual URL immediately upon receiving a
provisional server redirect in
`didReceiveServerRedirectForProvisionalNavigation:`. Because
browser-initiated pending items are returned by `GetVisibleItem()` while
loading, this allowed the omnibox to prematurely display the redirect
destination URL (and secure padlock) before the navigation ever
committed. If the destination document stalled or failed to commit, the
originating document remained painted while the omnibox displayed the
spoofed destination domain. This CL fixes the issue by:
1. Keeping the initial requested URL on the pending `NavigationItemImpl`
   during provisional redirects, while still tracking the updated URL on
   `NavigationContextImpl`.
2. Deferring the update of the navigation item's URL and virtual URL to
   commit time in `webPageChangedWithContext:webView:`.
3. Ensuring `context->SetHasCommitted(true)` in `didCommitNavigation:`
   correctly handles redirect commits.
Tests:
- Added `CRWKNavigationHandlerIntTest.VisibleItemDuringServerRedirect`,
  which verifies that `NavigationManager::GetVisibleItem()` continues to
  return the original requested URL during `DidRedirectNavigation` and
  only updates to the destination URL after the navigation finishes and
  commits.

Fixed: 546438368
Change-Id: I1a86863a40ad948f913782455c953677ce00843b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8281650
Commit-Queue: Mike Dougherty <[email protected]>
Reviewed-by: Justin Cohen <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1687338}
---
diff --git a/ios/web/navigation/crw_wk_navigation_handler.mm b/ios/web/navigation/crw_wk_navigation_handler.mm
index 1cd434e2..7786680 100644
--- a/ios/web/navigation/crw_wk_navigation_handler.mm
+++ b/ios/web/navigation/crw_wk_navigation_handler.mm
@@ -698,10 +698,6 @@
   // navigation. WKWebView allows multiple provisional navigations, while
   // Navigation Manager has only one pending navigation.
   if (item) {
-    if (!self.pendingNavigationInfo.unsafeRedirect) {
-      item->SetVirtualURL(webViewURL);
-      item->SetURL(webViewURL);
-    }
     // Redirects (3xx response code), must change POST requests to GETs.
     item->SetPostData(nil);
     item->ResetHttpRequestHeaders();
@@ -891,15 +887,16 @@
               ->mime_type());
     }
 
-    if ((pendingURL == webViewURL) || (context->IsLoadingHtmlString())) {
+    if (context->IsLoadingHtmlString() ||
+        (pendingItem &&
+         ((pendingURL == webViewURL) || (context->GetUrl() == webViewURL)))) {
       // Commit navigation if at least one of these is true:
-      //  - Navigation has pending item (this should always be true, but
-      //    pending item may not exist due to crbug.com/925304).
-      //  - Navigation is loadHTMLString:baseURL: navigation, which does not
-      //    create a pending item, but modifies committed item instead.
-      //  - Transition type is reload with Legacy Navigation Manager (Legacy
-      //    Navigation Manager does not create pending item for reload due to
-      //    crbug.com/676129)
+      //  - Navigation is a `loadHTMLString:baseURL:` navigation, which does not
+      //    create a pending item, but modifies the committed item instead.
+      //  - Navigation has a pending item (this should always be true, but
+      //    pending item may not exist due to crbug.com/925304) whose initial
+      //    URL (`pendingURL`) or redirected URL (`context->GetUrl()`) matches
+      //    `webViewURL`.
       context->SetHasCommitted(true);
     }
     self.webStateImpl->SetContentsMimeType(
@@ -2565,7 +2562,14 @@
   [self resetDocumentSpecificState];
 
   [self.delegate navigationHandlerDidStartLoading:self];
-  self.navigationManagerImpl->CommitPendingItem(context->ReleaseItem());
+  std::unique_ptr<web::NavigationItemImpl> item = context->ReleaseItem();
+  if (item && item->GetURL() != context->GetUrl()) {
+    if (item->GetVirtualURL() == item->GetURL()) {
+      item->SetVirtualURL(context->GetUrl());
+    }
+    item->SetURL(context->GetUrl());
+  }
+  self.navigationManagerImpl->CommitPendingItem(std::move(item));
   if (context->IsLoadingHtmlString()) {
     self.navigationManagerImpl->GetLastCommittedItem()->SetURL(
         context->GetUrl());
diff --git a/ios/web/navigation/crw_wk_navigation_handler_inttest.mm b/ios/web/navigation/crw_wk_navigation_handler_inttest.mm
index f5092cd..9e0a439 100644
--- a/ios/web/navigation/crw_wk_navigation_handler_inttest.mm
+++ b/ios/web/navigation/crw_wk_navigation_handler_inttest.mm
@@ -100,6 +100,40 @@
   ErrorType error_type_ = ErrorType::kNone;
 };
 
+// A WebStateObserver that tracks the url of the visible item during
+// DidRedirectNavigation.
+class RedirectVisibleItemObserver : public web::WebStateObserver {
+ public:
+  explicit RedirectVisibleItemObserver(web::WebState* web_state) {
+    scoped_observation_.Observe(web_state);
+  }
+
+  void DidRedirectNavigation(
+      web::WebState* web_state,
+      web::NavigationContext* navigation_context) override {
+    web::NavigationItem* visible_item =
+        web_state->GetNavigationManager()->GetVisibleItem();
+    ASSERT_TRUE(visible_item);
+    visible_url_during_redirect_ = visible_item->GetURL();
+    did_redirect_ = true;
+  }
+
+  void WebStateDestroyed(web::WebState* web_state) override {
+    scoped_observation_.Reset();
+  }
+
+  bool did_redirect() const { return did_redirect_; }
+  const GURL& visible_url_during_redirect() const {
+    return visible_url_during_redirect_;
+  }
+
+ private:
+  base::ScopedObservation<web::WebState, web::WebStateObserver>
+      scoped_observation_{this};
+  bool did_redirect_ = false;
+  GURL visible_url_during_redirect_;
+};
+
 }  // namespace
 
 namespace web {
@@ -301,4 +335,33 @@
   EXPECT_FALSE(test::IsWebViewContainingText(web_state(), kBlockedDataContent));
 }
 
+// Tests that GetVisibleItem() does not expose uncommitted redirect destinations
+// during provisional redirects, preventing address bar spoofing.
+TEST_F(CRWKNavigationHandlerIntTest, VisibleItemDuringServerRedirect) {
+  ASSERT_TRUE(server_.Start());
+  GURL destination_url(server_.GetURL("/echoall"));
+  GURL redirect_url(
+      server_.GetURL("/server-redirect?" + destination_url.spec()));
+
+  RedirectVisibleItemObserver observer(web_state());
+  ASSERT_TRUE(LoadUrl(redirect_url));
+
+  EXPECT_TRUE(observer.did_redirect());
+  // The visible item during provisional redirect must remain the initial
+  // requested URL to prevent address bar spoofing.
+  EXPECT_EQ(redirect_url, observer.visible_url_during_redirect());
+
+  // Once committed, the visible and last committed items reflect the
+  // destination.
+  NavigationItem* visible_item =
+      web_state()->GetNavigationManager()->GetVisibleItem();
+  ASSERT_TRUE(visible_item);
+  EXPECT_EQ(destination_url, visible_item->GetURL());
+
+  NavigationItem* committed_item =
+      web_state()->GetNavigationManager()->GetLastCommittedItem();
+  ASSERT_TRUE(committed_item);
+  EXPECT_EQ(destination_url, committed_item->GetURL());
+}
+
 }  // namespace web
diff --git a/ios/web/web_state/web_state_observer_inttest.mm b/ios/web/web_state/web_state_observer_inttest.mm
index 7c0b167..3e60236e 100644
--- a/ios/web/web_state/web_state_observer_inttest.mm
+++ b/ios/web/web_state/web_state_observer_inttest.mm
@@ -94,6 +94,12 @@
   EXPECT_EQ(expected_title, base::UTF16ToASCII(web_state->GetTitle()));
 }
 
+// Verifies correctness of WebState's visible URL.
+ACTION_P(VerifyVisibleURL, expected_url) {
+  WebState* web_state = arg0;
+  EXPECT_EQ(expected_url, web_state->GetVisibleURL());
+}
+
 // Verifies correctness of `NavigationContext` (`arg1`) for new page navigation
 // passed to `DidStartNavigation`. Stores `NavigationContext` in `context`
 // pointer.
@@ -2074,31 +2080,36 @@
                   _, RequestInfoMatch(expected_redirect_request_info), _))
       .WillOnce(
           RunOnceCallback<2>(WebStatePolicyDecider::PolicyDecision::Allow()));
-  EXPECT_CALL(observer_, DidRedirectNavigation(web_state(), _));
+  EXPECT_CALL(observer_, DidRedirectNavigation(web_state(), _))
+      .WillOnce(VerifyVisibleURL(url));
   EXPECT_CALL(*decider_,
               MockShouldAllowRequest(
                   _, RequestInfoMatch(expected_redirect_request_info), _))
       .WillOnce(
           RunOnceCallback<2>(WebStatePolicyDecider::PolicyDecision::Allow()));
-  EXPECT_CALL(observer_, DidRedirectNavigation(web_state(), _));
+  EXPECT_CALL(observer_, DidRedirectNavigation(web_state(), _))
+      .WillOnce(VerifyVisibleURL(url));
   EXPECT_CALL(*decider_,
               MockShouldAllowRequest(
                   _, RequestInfoMatch(expected_redirect_request_info), _))
       .WillOnce(
           RunOnceCallback<2>(WebStatePolicyDecider::PolicyDecision::Allow()));
-  EXPECT_CALL(observer_, DidRedirectNavigation(web_state(), _));
+  EXPECT_CALL(observer_, DidRedirectNavigation(web_state(), _))
+      .WillOnce(VerifyVisibleURL(url));
   EXPECT_CALL(*decider_,
               MockShouldAllowRequest(
                   _, RequestInfoMatch(expected_redirect_request_info), _))
       .WillOnce(
           RunOnceCallback<2>(WebStatePolicyDecider::PolicyDecision::Allow()));
-  EXPECT_CALL(observer_, DidRedirectNavigation(web_state(), _));
+  EXPECT_CALL(observer_, DidRedirectNavigation(web_state(), _))
+      .WillOnce(VerifyVisibleURL(url));
   EXPECT_CALL(*decider_,
               MockShouldAllowRequest(
                   _, RequestInfoMatch(expected_redirect_request_info), _))
       .WillOnce(
           RunOnceCallback<2>(WebStatePolicyDecider::PolicyDecision::Allow()));
-  EXPECT_CALL(observer_, DidRedirectNavigation(web_state(), _));
+  EXPECT_CALL(observer_, DidRedirectNavigation(web_state(), _))
+      .WillOnce(VerifyVisibleURL(url));
 
   EXPECT_CALL(*decider_, ShouldAllowResponse(
                              _, ResponseInfoMatch(expected_response_info), _))
@@ -2112,6 +2123,7 @@
   EXPECT_CALL(observer_,
               PageLoaded(web_state(), PageLoadCompletionStatus::SUCCESS));
   ASSERT_TRUE(LoadUrl(url));
+  EXPECT_EQ(redirect_url, web_state()->GetVisibleURL());
 }
 
 // Tests download navigation.
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.