Medium watch CVSS 4.8 chrome Authorization Error 🔧 Commit mapped

Overview

Medium
Severity
4.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactConfused deputy in PriceTracking
DescriptionConfused deputy in PriceTracking
ComponentPriceTracking
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.
Tracker513858387
Fix commitd1c54bd927b3 (chromium/src) +80/-4
CWECWE-441
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-16 (per vendor advisory)
Fix landed2026-08-04 (public git)
Disclosed2026-09-15
Patch gap42 days — fix public in source before the advisory

Background

`PriceNotificationsPriceTrackingMediator`
the iOS mediator that backs the price-tracking / price-notifications UI and drives navigation on behalf of the user when a tracked item is tapped.
`navigateToWebpageForURL:disposition:`
the mediator method that opens a URL taken from a tracked-item’s entryURL by calling self.webState->OpenURL(...).
Confused deputy
a trusted component (here the mediator) performing an action with its own elevated authority using an attacker-influenced input, so the attacker borrows that authority.
`WebState::OpenURL` / `OpenURLParams`
the web::WebState navigation entry point; the mediator invokes it with is_renderer_initiated=false and ui::PAGE_TRANSITION_GENERATED, marking the navigation as browser-initiated and therefore trusted.

Root Cause Analysis

Before the patch, navigateToWebpageForURL:disposition: passed the tracked item’s entryURL straight into self.webState->OpenURL(...) with no scheme check, and it did so as a browser-initiated navigation (is_renderer_initiated=false). Because the entryURL originates from data associated with a tracked product/bookmark rather than a vetted, freshly-parsed HTTP(S) address, a non-web scheme such as javascript:, chrome://, or file:// could be handed to the navigation stack while wearing the mediator’s browser-initiated trust — a classic confused-deputy condition.

The fix adds if (!URL.SchemeIsHTTPOrHTTPS()) { return; } as the first statement, so any non-HTTP/HTTPS URL early-exits before OpenURL is ever reached; this is a tightening of an input that previously had no restriction.

The patch also adds if (!self.webState) return; in both navigateToWebpageForURL:disposition: and navigateToBookmarks (and makes price_notifications_view_coordinator.mm pass nullptr instead of dereferencing a null webState), converting a raw _webState-> dereference into a guarded self.webState-> call to prevent a null-dereference crash when the web state has gone away. The scheme guard runs before both the null check and the sink, so the ordering of the early-exit path correctly blocks the dangerous navigation.

Key insight
The mediator forwarded an attacker-influenceable entryURL into a browser-initiated OpenURL without validating the scheme, letting a non-web URL inherit the mediator’s trust; the fix restricts the sink to HTTP/HTTPS URLs (and null-guards the web state) so only ordinary web navigations reach OpenURL.

Attack Path

  1. Plant a malicious tracked-item URL An attacker arranges for a price-tracked item whose entryURL is a non-web scheme (e.g. javascript:alert(1), chrome://version, or file:///etc/passwd), the exact schemes the added test enumerates as now-blocked.
  2. User invokes navigation from the price-tracking UI The user taps the tracked item, and the UI calls navigateToWebpageForItem:, which forwards item.entryURL into navigateToWebpageForURL:disposition:.
  3. Mediator opens the URL with browser-initiated trust Pre-patch, the method calls self.webState->OpenURL(...) with is_renderer_initiated=false and PAGE_TRANSITION_GENERATED, so the non-web URL is dispatched as a trusted navigation rather than an untrusted renderer request.
  4. Elevated-context navigation / script execution The browser-initiated javascript:, chrome://, or file:// navigation runs in a more privileged context than a renderer-initiated one would be allowed, potentially executing script or reaching privileged pages/local files (inferred — the patch demonstrates only that the URL reaches/does-not-reach OpenURL, not the resulting execution primitive).

Impact Assessment

An attacker who can control a tracked item’s entryURL can cause the price-tracking mediator to issue a browser-initiated navigation to a non-web scheme, gaining a confused-deputy navigation (e.g. javascript:/chrome:///file://) in the browser UI context on iOS Chrome. Escalation to actual script execution, access to privileged chrome:// surfaces, or local-file disclosure is (inferred) and depends on downstream handling the patch does not show. No exploited-in-the-wild status or severity rating is provided by the material supplied; the secondary null-webState guard additionally prevents a crash when the web state has been torn down.

Changed Functions

FunctionChangeNotes
if
ios/chrome/browser/price_notifications/ui_bundled/price_notifications_price_tracking_mediator.mm
modified
TestFakeWebState
ios/chrome/browser/price_notifications/ui_bundled/price_notifications_price_tracking_mediator_unittest.mm
modified

Files Changed

  • ios/chrome/browser/price_notifications/ui_bundled/price_notifications_price_tracking_mediator_unittest.mm
  • ios/chrome/browser/price_notifications/ui_bundled/price_notifications_view_coordinator.mm
  • ios/chrome/browser/price_notifications/ui_bundled/price_notifications_price_tracking_mediator.mm

Audit Directions

  • Unvalidated URL into browser-initiated `OpenURL`
    Audit every UI mediator/coordinator that calls WebState::OpenURL (or similar navigation APIs) with is_renderer_initiated=false on a URL sourced from stored items, bookmarks, notifications, or server data, and confirm a SchemeIsHTTPOrHTTPS()-style scheme allowlist runs first.
  • Raw web-state dereference without a lifetime guard
    Look for _webState->/webState-> dereferences (especially through weak pointers) in mediators that outlive their web state, and verify a null check precedes each use.
  • Attacker-influenceable `entryURL`/item fields feeding navigation
    Trace where tracked-item, notification, or feed model fields flow into navigation, image, or resource loads, and ensure schemes are constrained at the trust boundary rather than assumed to be web URLs.
From d1c54bd927b3bfe174efd5327fd75618f57e7113 Mon Sep 17 00:00:00 2001
From: David M <davidjm@google.com>
Date: Mon, 03 Aug 2026 18:42:45 -0700
Subject: [PATCH] Add scheme validation and webState null checks in PriceNotificationsPriceTrackingMediator

Add scheme validation and webState null checks to block non-HTTP/HTTPS
URLs (such as javascript: or chrome://) by adding HTTP/HTTPS URL scheme
validation to navigateToWebpageForURL.

Also add null checks for self.webState before dereferencing it in
navigateToBookmarks and navigateToWebpageForURL.

Bug: 513858387
Change-Id: If847cb64202b428a1d75bdfe7878a213d975fc97
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8175357
Reviewed-by: Daniel White <danieltwhite@google.com>
Commit-Queue: David Maunder <davidjm@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1673027}
---
diff --git a/ios/chrome/browser/price_notifications/ui_bundled/price_notifications_price_tracking_mediator.mm b/ios/chrome/browser/price_notifications/ui_bundled/price_notifications_price_tracking_mediator.mm
index 6015dd4..8949b1a3 100644
--- a/ios/chrome/browser/price_notifications/ui_bundled/price_notifications_price_tracking_mediator.mm
+++ b/ios/chrome/browser/price_notifications/ui_bundled/price_notifications_price_tracking_mediator.mm
@@ -180,7 +180,10 @@
 
 - (void)navigateToBookmarks {
   [self.handler hidePriceTrackedItems];
-  GURL URL = _webState->GetLastCommittedURL();
+  if (!self.webState) {
+    return;
+  }
+  GURL URL = self.webState->GetLastCommittedURL();
   [self.bookmarksHandler showBookmarkInBookmarksUI:URL];
 }
 
@@ -545,6 +548,12 @@
 
 - (void)navigateToWebpageForURL:(const GURL&)URL
                     disposition:(WindowOpenDisposition)disposition {
+  if (!URL.SchemeIsHTTPOrHTTPS()) {
+    return;
+  }
+  if (!self.webState) {
+    return;
+  }
   self.webState->OpenURL(web::WebState::OpenURLParams(
       URL, web::Referrer(), disposition, ui::PAGE_TRANSITION_GENERATED,
       /*is_renderer_initiated=*/false));
diff --git a/ios/chrome/browser/price_notifications/ui_bundled/price_notifications_view_coordinator.mm b/ios/chrome/browser/price_notifications/ui_bundled/price_notifications_view_coordinator.mm
index 42cc6b6..5acabda 100644
--- a/ios/chrome/browser/price_notifications/ui_bundled/price_notifications_view_coordinator.mm
+++ b/ios/chrome/browser/price_notifications/ui_bundled/price_notifications_view_coordinator.mm
@@ -101,7 +101,7 @@
       initWithShoppingService:shoppingService
                 bookmarkModel:bookmarkModel
                  imageFetcher:std::move(imageFetcher)
-                     webState:webState->GetWeakPtr()
+                     webState:webState ? webState->GetWeakPtr() : nullptr
       pushNotificationService:pushNotificationService];
   self.mediator.consumer = self.tableViewController;
   self.mediator.presenter = self;
diff --git a/ios/chrome/browser/price_notifications/ui_bundled/price_notifications_price_tracking_mediator_unittest.mm b/ios/chrome/browser/price_notifications/ui_bundled/price_notifications_price_tracking_mediator_unittest.mm
index f025286..912abec 100644
--- a/ios/chrome/browser/price_notifications/ui_bundled/price_notifications_price_tracking_mediator_unittest.mm
+++ b/ios/chrome/browser/price_notifications/ui_bundled/price_notifications_price_tracking_mediator_unittest.mm
@@ -58,6 +58,21 @@
 const char kBookmarkTitle[] = "My product title";
 constexpr uint64_t kClusterId = 12345L;
 
+// A FakeWebState that records parameters passed to OpenURL.
+class TestFakeWebState : public web::FakeWebState {
+ public:
+  void OpenURL(const web::WebState::OpenURLParams& params) override {
+    last_open_url_params_ =
+        std::make_unique<web::WebState::OpenURLParams>(params);
+  }
+  web::WebState::OpenURLParams* last_open_url_params() {
+    return last_open_url_params_.get();
+  }
+
+ private:
+  std::unique_ptr<web::WebState::OpenURLParams> last_open_url_params_;
+};
+
 PriceInsightsItem* GetPriceInsightsItem() {
   PriceInsightsItem* item = [[PriceInsightsItem alloc] init];
   item.title = base::SysUTF8ToNSString(kBookmarkTitle);
@@ -137,7 +152,7 @@
     browser_list_ = BrowserListFactory::GetForProfile(test_profile);
     browser_ = std::make_unique<TestBrowser>(test_profile);
     browser_list_->AddBrowser(browser_.get());
-    web_state_ = std::make_unique<web::FakeWebState>();
+    web_state_ = std::make_unique<TestFakeWebState>();
     std::unique_ptr<web::FakeNavigationManager> navigation_manager =
         std::make_unique<web::FakeNavigationManager>();
     navigation_manager->AddItem(GURL(kTestUrl), ui::PAGE_TRANSITION_LINK);
@@ -187,7 +202,7 @@
   TestProfileManagerIOS profile_manager_;
   std::unique_ptr<Browser> browser_;
   PriceNotificationsPriceTrackingMediator* mediator_;
-  std::unique_ptr<web::FakeWebState> web_state_;
+  std::unique_ptr<TestFakeWebState> web_state_;
   raw_ptr<commerce::MockShoppingService> shopping_service_;
   raw_ptr<bookmarks::BookmarkModel> bookmark_model_;
   raw_ptr<BrowserList> browser_list_;
@@ -526,3 +541,55 @@
 
   EXPECT_OCMOCK_VERIFY(mock_notification_center_);
 }
+
+TEST_F(PriceNotificationsPriceTrackingMediatorTest,
+       NavigateToWebpageForURLWithHTTPOrHTTPSAllowed) {
+  PriceNotificationsTableViewItem* item =
+      [[PriceNotificationsTableViewItem alloc] init];
+  item.tracking = YES;
+  item.entryURL = GURL("https://www.merchant.com");
+  [mediator_ navigateToWebpageForItem:item];
+
+  EXPECT_NE(web_state_->last_open_url_params(), nullptr);
+  EXPECT_EQ(web_state_->last_open_url_params()->url,
+            GURL("https://www.merchant.com"));
+}
+
+TEST_F(PriceNotificationsPriceTrackingMediatorTest,
+       NavigateToWebpageForURLWithNonHTTPOrHTTPSBlocked) {
+  PriceNotificationsTableViewItem* item =
+      [[PriceNotificationsTableViewItem alloc] init];
+  item.tracking = YES;
+
+  item.entryURL = GURL("javascript:alert(1)");
+  [mediator_ navigateToWebpageForItem:item];
+  EXPECT_EQ(web_state_->last_open_url_params(), nullptr);
+
+  item.entryURL = GURL("chrome://version");
+  [mediator_ navigateToWebpageForItem:item];
+  EXPECT_EQ(web_state_->last_open_url_params(), nullptr);
+
+  item.entryURL = GURL("file:///etc/passwd");
+  [mediator_ navigateToWebpageForItem:item];
+  EXPECT_EQ(web_state_->last_open_url_params(), nullptr);
+}
+
+TEST_F(PriceNotificationsPriceTrackingMediatorTest,
+       NavigateToWebpageForURLWithNullWebState) {
+  web_state_.reset();
+  PriceNotificationsTableViewItem* item =
+      [[PriceNotificationsTableViewItem alloc] init];
+  item.tracking = YES;
+  item.entryURL = GURL("https://www.merchant.com");
+
+  // Should not crash.
+  [mediator_ navigateToWebpageForItem:item];
+}
+
+TEST_F(PriceNotificationsPriceTrackingMediatorTest,
+       NavigateToBookmarksWithNullWebState) {
+  web_state_.reset();
+
+  // Should not crash.
+  [mediator_ navigateToBookmarks];
+}
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/ios/chrome/browser/price_notifications/ui_bundled/price_notifications_price_tracking_mediator_unittest.mm b/ios/chrome/browser/price_notifications/ui_bundled/price_notifications_price_tracking_mediator_unittest.mm
index f025286..912abec 100644
--- a/ios/chrome/browser/price_notifications/ui_bundled/price_notifications_price_tracking_mediator_unittest.mm
+++ b/ios/chrome/browser/price_notifications/ui_bundled/price_notifications_price_tracking_mediator_unittest.mm
@@ -58,6 +58,21 @@
 const char kBookmarkTitle[] = "My product title";
 constexpr uint64_t kClusterId = 12345L;
 
+// A FakeWebState that records parameters passed to OpenURL.
+class TestFakeWebState : public web::FakeWebState {
+ public:
+  void OpenURL(const web::WebState::OpenURLParams& params) override {
+    last_open_url_params_ =
+        std::make_unique<web::WebState::OpenURLParams>(params);
+  }
+  web::WebState::OpenURLParams* last_open_url_params() {
+    return last_open_url_params_.get();
+  }
+
+ private:
+  std::unique_ptr<web::WebState::OpenURLParams> last_open_url_params_;
+};
+
 PriceInsightsItem* GetPriceInsightsItem() {
   PriceInsightsItem* item = [[PriceInsightsItem alloc] init];
   item.title = base::SysUTF8ToNSString(kBookmarkTitle);
@@ -137,7 +152,7 @@
     browser_list_ = BrowserListFactory::GetForProfile(test_profile);
     browser_ = std::make_unique<TestBrowser>(test_profile);
     browser_list_->AddBrowser(browser_.get());
-    web_state_ = std::make_unique<web::FakeWebState>();
+    web_state_ = std::make_unique<TestFakeWebState>();
     std::unique_ptr<web::FakeNavigationManager> navigation_manager =
         std::make_unique<web::FakeNavigationManager>();
     navigation_manager->AddItem(GURL(kTestUrl), ui::PAGE_TRANSITION_LINK);
@@ -187,7 +202,7 @@
   TestProfileManagerIOS profile_manager_;
   std::unique_ptr<Browser> browser_;
   PriceNotificationsPriceTrackingMediator* mediator_;
-  std::unique_ptr<web::FakeWebState> web_state_;
+  std::unique_ptr<TestFakeWebState> web_state_;
   raw_ptr<commerce::MockShoppingService> shopping_service_;
   raw_ptr<bookmarks::BookmarkModel> bookmark_model_;
   raw_ptr<BrowserList> browser_list_;
@@ -526,3 +541,55 @@
 
   EXPECT_OCMOCK_VERIFY(mock_notification_center_);
 }
+
+TEST_F(PriceNotificationsPriceTrackingMediatorTest,
+       NavigateToWebpageForURLWithHTTPOrHTTPSAllowed) {
+  PriceNotificationsTableViewItem* item =
+      [[PriceNotificationsTableViewItem alloc] init];
+  item.tracking = YES;
+  item.entryURL = GURL("https://www.merchant.com");
+  [mediator_ navigateToWebpageForItem:item];
+
+  EXPECT_NE(web_state_->last_open_url_params(), nullptr);
+  EXPECT_EQ(web_state_->last_open_url_params()->url,
+            GURL("https://www.merchant.com"));
+}
+
+TEST_F(PriceNotificationsPriceTrackingMediatorTest,
+       NavigateToWebpageForURLWithNonHTTPOrHTTPSBlocked) {
+  PriceNotificationsTableViewItem* item =
+      [[PriceNotificationsTableViewItem alloc] init];
+  item.tracking = YES;
+
+  item.entryURL = GURL("javascript:alert(1)");
+  [mediator_ navigateToWebpageForItem:item];
+  EXPECT_EQ(web_state_->last_open_url_params(), nullptr);
+
+  item.entryURL = GURL("chrome://version");
+  [mediator_ navigateToWebpageForItem:item];
+  EXPECT_EQ(web_state_->last_open_url_params(), nullptr);
+
+  item.entryURL = GURL("file:///etc/passwd");
+  [mediator_ navigateToWebpageForItem:item];
+  EXPECT_EQ(web_state_->last_open_url_params(), nullptr);
+}
+
+TEST_F(PriceNotificationsPriceTrackingMediatorTest,
+       NavigateToWebpageForURLWithNullWebState) {
+  web_state_.reset();
+  PriceNotificationsTableViewItem* item =
+      [[PriceNotificationsTableViewItem alloc] init];
+  item.tracking = YES;
+  item.entryURL = GURL("https://www.merchant.com");
+
+  // Should not crash.
+  [mediator_ navigateToWebpageForItem:item];
+}
+
+TEST_F(PriceNotificationsPriceTrackingMediatorTest,
+       NavigateToBookmarksWithNullWebState) {
+  web_state_.reset();
+
+  // Should not crash.
+  [mediator_ navigateToBookmarks];
+}
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.