Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Chrome for iOS
DescriptionInsufficient validation of untrusted input in Chrome for iOS
ComponentChrome for iOS
Bug ClassLogic Error
Tracker513855922
Fix commitb752b1f06c15 (chromium/src) +130/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
FakeContentNotificationService
ios/chrome/browser/content_notification/model/content_notification_client_unittest.mm
modified
if
ios/chrome/browser/content_notification/model/content_notification_client_unittest.mm
modified
ContentNotificationClientTest
ios/chrome/browser/content_notification/model/content_notification_client_unittest.mm
modified
ContentNotificationClientTest
ios/chrome/browser/content_notification/model/content_notification_client_unittest.mm
modified
TEST_F
ios/chrome/browser/content_notification/model/content_notification_client_unittest.mm
modified

Files Changed

  • ios/chrome/browser/content_notification/model/BUILD.gn
  • ios/chrome/browser/content_notification/model/content_notification_client.mm
  • ios/chrome/browser/content_notification/model/content_notification_client_unittest.mm
From b752b1f06c1590497c16c0d9c1afec84ca6b76b2 Mon Sep 17 00:00:00 2001
From: Guillaume Jenkins <[email protected]>
Date: Wed, 03 Jun 2026 12:37:59 -0700
Subject: [PATCH] [iOS][ContentNotifications] Validate destination URL scheme

The Content Notification client extracts a destination URL from push
notification payloads and initiates a trusted navigation when the user
interacts with the notification. This CL introduces scheme validation in
ContentNotificationClient before navigating, ensuring that only valid
HTTP(S) schemes are loaded in the browser.

Fixed: 513855922
Change-Id: I1c7bcfe1e564c175eea9574800a8065cc0036ab3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7887734
Reviewed-by: Scott Yoder <[email protected]>
Auto-Submit: Guillaume Jenkins <[email protected]>
Commit-Queue: Scott Yoder <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1641117}
---

diff --git a/ios/chrome/browser/content_notification/model/BUILD.gn b/ios/chrome/browser/content_notification/model/BUILD.gn
index e2e6c00..2cb07c2 100644
--- a/ios/chrome/browser/content_notification/model/BUILD.gn
+++ b/ios/chrome/browser/content_notification/model/BUILD.gn
@@ -113,6 +113,7 @@
   deps = [
     ":constants",
     ":content_notification_client",
+    ":content_notification_service",
     ":content_notification_service_factory",
     "//base",
     "//base/test:test_support",
diff --git a/ios/chrome/browser/content_notification/model/content_notification_client.mm b/ios/chrome/browser/content_notification/model/content_notification_client.mm
index 5c2a2c3..daa3182 100644
--- a/ios/chrome/browser/content_notification/model/content_notification_client.mm
+++ b/ios/chrome/browser/content_notification/model/content_notification_client.mm
@@ -100,7 +100,7 @@
         kContentNotificationActionHistogramName,
         NotificationActionType::kNotificationActionTypeOpened);
     const GURL& url = contentNotificationService->GetDestinationUrl(payload);
-    if (url.is_empty()) {
+    if (url.is_empty() || !url.is_valid() || !url.SchemeIsHTTPOrHTTPS()) {
       base::UmaHistogramBoolean("ContentNotifications.OpenURLAction.HasURL",
                                 false);
       return true;
diff --git a/ios/chrome/browser/content_notification/model/content_notification_client_unittest.mm b/ios/chrome/browser/content_notification/model/content_notification_client_unittest.mm
index 26494715..1b9ce0d 100644
--- a/ios/chrome/browser/content_notification/model/content_notification_client_unittest.mm
+++ b/ios/chrome/browser/content_notification/model/content_notification_client_unittest.mm
@@ -6,9 +6,12 @@
 
 #import <UserNotifications/UserNotifications.h>
 
+#import "base/strings/sys_string_conversions.h"
 #import "base/test/metrics/histogram_tester.h"
 #import "base/threading/thread_restrictions.h"
 #import "components/prefs/scoped_user_pref_update.h"
+#import "ios/chrome/browser/content_notification/model/content_notification_service.h"
+#import "ios/chrome/browser/content_notification/model/content_notification_service_factory.h"
 #import "ios/chrome/browser/default_browser/model/promo_source.h"
 #import "ios/chrome/browser/default_browser/model/utils.h"
 #import "ios/chrome/browser/default_browser/model/utils_test_support.h"
@@ -24,6 +27,7 @@
 #import "ios/chrome/browser/shared/model/profile/test/test_profile_manager_ios.h"
 #import "ios/chrome/browser/shared/public/commands/browser_coordinator_commands.h"
 #import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
+#import "ios/chrome/browser/shared/public/commands/open_new_tab_command.h"
 #import "ios/chrome/browser/shared/public/commands/scene_commands.h"
 #import "ios/chrome/browser/shared/public/commands/settings_commands.h"
 #import "ios/chrome/test/ios_chrome_scoped_testing_local_state.h"
@@ -34,11 +38,46 @@
 #import "third_party/ocmock/OCMock/OCMock.h"
 #import "third_party/ocmock/gtest_support.h"
 
+namespace {
+
+class FakeContentNotificationService : public ContentNotificationService {
+ public:
+  FakeContentNotificationService() = default;
+  ~FakeContentNotificationService() override = default;
+
+  GURL GetDestinationUrl(NSDictionary<NSString*, id>* payload) override {
+    NSString* url_string = payload[@"destination_url"];
+    if (url_string) {
+      return GURL(base::SysNSStringToUTF8(url_string));
+    }
+    return GURL::EmptyGURL();
+  }
+
+  NSDictionary<NSString*, NSString*>* GetFeedbackPayload(
+      NSDictionary<NSString*, id>* payload) override {
+    return nil;
+  }
+
+  void SendNAUForConfiguration(
+      ContentNotificationNAUConfiguration* configuration) override {}
+};
+
+std::unique_ptr<KeyedService> CreateFakeContentNotificationService(
+    ProfileIOS* profile) {
+  return std::make_unique<FakeContentNotificationService>();
+}
+
+}  // namespace
+
 class ContentNotificationClientTest : public PlatformTest {
  protected:
   ContentNotificationClientTest() {
+    TestProfileIOS::Builder builder;
+    builder.AddTestingFactory(
+        ContentNotificationServiceFactory::GetInstance(),
+        base::BindRepeating(&CreateFakeContentNotificationService));
     ProfileIOS* profile =
-        profile_manager_.AddProfileWithBuilder(TestProfileIOS::Builder());
+        profile_manager_.AddProfileWithBuilder(std::move(builder));
     BrowserList* list = BrowserListFactory::GetForProfile(profile);
     mock_scene_state_ = OCMClassMock([SceneState class]);
     OCMStub([mock_scene_state_ activationLevel])
@@ -82,6 +121,24 @@
     return request;
   }
 
+  id CreateMockResponse(NSString* action_identifier,
+                        NSDictionary<NSString*, id>* payload) {
+    id mock_response = OCMClassMock([UNNotificationResponse class]);
+    id mock_notification = OCMClassMock([UNNotification class]);
+    id mock_request = OCMClassMock([UNNotificationRequest class]);
+    id mock_content = OCMClassMock([UNNotificationContent class]);
+
+    OCMStub([mock_response notification]).andReturn(mock_notification);
+    OCMStub([mock_notification request]).andReturn(mock_request);
+    OCMStub([mock_request content]).andReturn(mock_content);
+    OCMStub([mock_response actionIdentifier]).andReturn(action_identifier);
+    OCMStub([mock_content categoryIdentifier])
+        .andReturn(kContentNotificationFeedbackCategoryIdentifier);
+    OCMStub([mock_content userInfo]).andReturn(payload);
+
+    return mock_response;
+  }
+
   web::WebTaskEnvironment task_environment_;
   IOSChromeScopedTestingLocalState scoped_testing_local_state_;
   TestProfileManagerIOS profile_manager_;
@@ -108,3 +165,73 @@
   EXPECT_EQ(secondaryActions.firstObject.identifier,
             kContentNotificationFeedbackCategoryIdentifier);
 }
+
+// Tests that the client correctly loads a valid HTTP URL and records the
+// appropriate histograms.
+TEST_F(ContentNotificationClientTest, HandleNotificationInteractionValidURL) {
+  base::HistogramTester histogram_tester;
+  id mock_scene_commands = OCMProtocolMock(@protocol(SceneCommands));
+  [browser_->GetCommandDispatcher()
+      startDispatchingToTarget:mock_scene_commands
+                   forProtocol:@protocol(SceneCommands)];
+
+  OCMExpect([mock_scene_commands
+      openURLInNewTab:[OCMArg checkWithBlock:^BOOL(OpenNewTabCommand* command) {
+        return command.URL == GURL("http://www.example.com/");
+      }]]);
+
+  NSDictionary<NSString*, id>* payload =
+      @{@"destination_url" : @"http://www.example.com/"};
+  id mock_response =
+      CreateMockResponse(UNNotificationDefaultActionIdentifier, payload);
+
+  EXPECT_TRUE(client_->HandleNotificationInteraction(mock_response));
+  EXPECT_OCMOCK_VERIFY(mock_scene_commands);
+
+  histogram_tester.ExpectUniqueSample(
+      "ContentNotifications.OpenURLAction.HasURL", true, 1);
+}
+
+// Tests that the client filters out invalid/non-HTTP/HTTPS URLs (like
+// chrome://) and records HasURL as false.
+TEST_F(ContentNotificationClientTest, HandleNotificationInteractionChromeURL) {
+  base::HistogramTester histogram_tester;
+  id mock_scene_commands = OCMProtocolMock(@protocol(SceneCommands));
+  [browser_->GetCommandDispatcher()
+      startDispatchingToTarget:mock_scene_commands
+                   forProtocol:@protocol(SceneCommands)];
+
+  OCMReject([mock_scene_commands openURLInNewTab:[OCMArg any]]);
+
+  NSDictionary<NSString*, id>* payload =
+      @{@"destination_url" : @"chrome://settings"};
+  id mock_response =
+      CreateMockResponse(UNNotificationDefaultActionIdentifier, payload);
+
+  EXPECT_TRUE(client_->HandleNotificationInteraction(mock_response));
+  EXPECT_OCMOCK_VERIFY(mock_scene_commands);
+
+  histogram_tester.ExpectUniqueSample(
+      "ContentNotifications.OpenURLAction.HasURL", false, 1);
+}
+
+// Tests that the client filters out invalid URLs and records HasURL as false.
+TEST_F(ContentNotificationClientTest, HandleNotificationInteractionInvalidURL) {
+  base::HistogramTester histogram_tester;
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/ios/chrome/browser/content_notification/model/content_notification_client_unittest.mm b/ios/chrome/browser/content_notification/model/content_notification_client_unittest.mm
index 26494715..1b9ce0d 100644
--- a/ios/chrome/browser/content_notification/model/content_notification_client_unittest.mm
+++ b/ios/chrome/browser/content_notification/model/content_notification_client_unittest.mm
@@ -6,9 +6,12 @@
 
 #import <UserNotifications/UserNotifications.h>
 
+#import "base/strings/sys_string_conversions.h"
 #import "base/test/metrics/histogram_tester.h"
 #import "base/threading/thread_restrictions.h"
 #import "components/prefs/scoped_user_pref_update.h"
+#import "ios/chrome/browser/content_notification/model/content_notification_service.h"
+#import "ios/chrome/browser/content_notification/model/content_notification_service_factory.h"
 #import "ios/chrome/browser/default_browser/model/promo_source.h"
 #import "ios/chrome/browser/default_browser/model/utils.h"
 #import "ios/chrome/browser/default_browser/model/utils_test_support.h"
@@ -24,6 +27,7 @@
 #import "ios/chrome/browser/shared/model/profile/test/test_profile_manager_ios.h"
 #import "ios/chrome/browser/shared/public/commands/browser_coordinator_commands.h"
 #import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
+#import "ios/chrome/browser/shared/public/commands/open_new_tab_command.h"
 #import "ios/chrome/browser/shared/public/commands/scene_commands.h"
 #import "ios/chrome/browser/shared/public/commands/settings_commands.h"
 #import "ios/chrome/test/ios_chrome_scoped_testing_local_state.h"
@@ -34,11 +38,46 @@
 #import "third_party/ocmock/OCMock/OCMock.h"
 #import "third_party/ocmock/gtest_support.h"
 
+namespace {
+
+class FakeContentNotificationService : public ContentNotificationService {
+ public:
+  FakeContentNotificationService() = default;
+  ~FakeContentNotificationService() override = default;
+
+  GURL GetDestinationUrl(NSDictionary<NSString*, id>* payload) override {
+    NSString* url_string = payload[@"destination_url"];
+    if (url_string) {
+      return GURL(base::SysNSStringToUTF8(url_string));
+    }
+    return GURL::EmptyGURL();
+  }
+
+  NSDictionary<NSString*, NSString*>* GetFeedbackPayload(
+      NSDictionary<NSString*, id>* payload) override {
+    return nil;
+  }
+
+  void SendNAUForConfiguration(
+      ContentNotificationNAUConfiguration* configuration) override {}
+};
+
+std::unique_ptr<KeyedService> CreateFakeContentNotificationService(
+    ProfileIOS* profile) {
+  return std::make_unique<FakeContentNotificationService>();
+}
+
+}  // namespace
+
 class ContentNotificationClientTest : public PlatformTest {
  protected:
   ContentNotificationClientTest() {
+    TestProfileIOS::Builder builder;
+    builder.AddTestingFactory(
+        ContentNotificationServiceFactory::GetInstance(),
+        base::BindRepeating(&CreateFakeContentNotificationService));
     ProfileIOS* profile =
-        profile_manager_.AddProfileWithBuilder(TestProfileIOS::Builder());
+        profile_manager_.AddProfileWithBuilder(std::move(builder));
     BrowserList* list = BrowserListFactory::GetForProfile(profile);
     mock_scene_state_ = OCMClassMock([SceneState class]);
     OCMStub([mock_scene_state_ activationLevel])
@@ -82,6 +121,24 @@
     return request;
   }
 
+  id CreateMockResponse(NSString* action_identifier,
+                        NSDictionary<NSString*, id>* payload) {
+    id mock_response = OCMClassMock([UNNotificationResponse class]);
+    id mock_notification = OCMClassMock([UNNotification class]);
+    id mock_request = OCMClassMock([UNNotificationRequest class]);
+    id mock_content = OCMClassMock([UNNotificationContent class]);
+
+    OCMStub([mock_response notification]).andReturn(mock_notification);
+    OCMStub([mock_notification request]).andReturn(mock_request);
+    OCMStub([mock_request content]).andReturn(mock_content);
+    OCMStub([mock_response actionIdentifier]).andReturn(action_identifier);
+    OCMStub([mock_content categoryIdentifier])
+        .andReturn(kContentNotificationFeedbackCategoryIdentifier);
+    OCMStub([mock_content userInfo]).andReturn(payload);
+
+    return mock_response;
+  }
+
   web::WebTaskEnvironment task_environment_;
   IOSChromeScopedTestingLocalState scoped_testing_local_state_;
   TestProfileManagerIOS profile_manager_;
@@ -108,3 +165,73 @@
   EXPECT_EQ(secondaryActions.firstObject.identifier,
             kContentNotificationFeedbackCategoryIdentifier);
 }
+
+// Tests that the client correctly loads a valid HTTP URL and records the
+// appropriate histograms.
+TEST_F(ContentNotificationClientTest, HandleNotificationInteractionValidURL) {
+  base::HistogramTester histogram_tester;
+  id mock_scene_commands = OCMProtocolMock(@protocol(SceneCommands));
+  [browser_->GetCommandDispatcher()
+      startDispatchingToTarget:mock_scene_commands
+                   forProtocol:@protocol(SceneCommands)];
+
+  OCMExpect([mock_scene_commands
+      openURLInNewTab:[OCMArg checkWithBlock:^BOOL(OpenNewTabCommand* command) {
+        return command.URL == GURL("http://www.example.com/");
+      }]]);
+
+  NSDictionary<NSString*, id>* payload =
+      @{@"destination_url" : @"http://www.example.com/"};
+  id mock_response =
+      CreateMockResponse(UNNotificationDefaultActionIdentifier, payload);
+
+  EXPECT_TRUE(client_->HandleNotificationInteraction(mock_response));
+  EXPECT_OCMOCK_VERIFY(mock_scene_commands);
+
+  histogram_tester.ExpectUniqueSample(
+      "ContentNotifications.OpenURLAction.HasURL", true, 1);
+}
+
+// Tests that the client filters out invalid/non-HTTP/HTTPS URLs (like
+// chrome://) and records HasURL as false.
+TEST_F(ContentNotificationClientTest, HandleNotificationInteractionChromeURL) {
+  base::HistogramTester histogram_tester;
+  id mock_scene_commands = OCMProtocolMock(@protocol(SceneCommands));
+  [browser_->GetCommandDispatcher()
+      startDispatchingToTarget:mock_scene_commands
+                   forProtocol:@protocol(SceneCommands)];
+
+  OCMReject([mock_scene_commands openURLInNewTab:[OCMArg any]]);
+
+  NSDictionary<NSString*, id>* payload =
+      @{@"destination_url" : @"chrome://settings"};
+  id mock_response =
+      CreateMockResponse(UNNotificationDefaultActionIdentifier, payload);
+
+  EXPECT_TRUE(client_->HandleNotificationInteraction(mock_response));
+  EXPECT_OCMOCK_VERIFY(mock_scene_commands);
+
+  histogram_tester.ExpectUniqueSample(
+      "ContentNotifications.OpenURLAction.HasURL", false, 1);
+}
+
+// Tests that the client filters out invalid URLs and records HasURL as false.
+TEST_F(ContentNotificationClientTest, HandleNotificationInteractionInvalidURL) {
+  base::HistogramTester histogram_tester;
+  id mock_scene_commands = OCMProtocolMock(@protocol(SceneCommands));
+  [browser_->GetCommandDispatcher()
+      startDispatchingToTarget:mock_scene_commands
+                   forProtocol:@protocol(SceneCommands)];
+
+  OCMReject([mock_scene_commands openURLInNewTab:[OCMArg any]]);
+
+  NSDictionary<NSString*, id>* payload = @{@"destination_url" : @"invalid_url"};
+  id mock_response =
+      CreateMockResponse(UNNotificationDefaultActionIdentifier, payload);
+
+  EXPECT_TRUE(client_->HandleNotificationInteraction(mock_response));
+  EXPECT_OCMOCK_VERIFY(mock_scene_commands);
+
+  histogram_tester.ExpectUniqueSample(
+      "ContentNotifications.OpenURLAction.HasURL", false, 1);
+}
Loading diff…

Original Bug Report

reported by [email protected]

Potential arbitrary privileged URL navigation via Content Notification interaction 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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: The Content Notification system on iOS fails to validate the scheme of URLs extracted from push payloads before initiating a navigation. This could allow an attacker to trigger browser-initiated navigations to privileged pages like chrome://.

Affected files:

  • ios/chrome/browser/content_notification/model/content_notification_client.mm

Estimated timestamp from git blame: 2024-04-03

A potential security bypass exists in the ContentNotificationClient component on iOS. When a user interacts with a content notification (e.g., tapping the notification body), the browser extracts a destination URL from the push payload and opens it in a new tab without sufficient scheme validation.

Technical Details

In ios/chrome/browser/content_notification/model/content_notification_client.mm, the method HandleNotificationInteraction processes notification responses. It retrieves a destination URL from the notification’s userInfo dictionary via the ContentNotificationService:

// ios/chrome/browser/content_notification/model/content_notification_client.mm:102
const GURL& url = contentNotificationService->GetDestinationUrl(payload);
if (url.is_empty()) {
  // ...
  return true;
}
// ...
LoadUrlInNewTab(url);

The implementation only checks if the URL is empty but fails to verify that the scheme is http or https.

The subsequent call to LoadUrlInNewTab (inherited from PushNotificationClient) utilizes the [OpenNewTabCommand commandWithURLFromChrome:url] initializer. This constructor sets an internal fromChrome flag to YES and leads to a navigation with the ui::PAGE_TRANSITION_TYPED transition type.

In the iOS web layer, specifically within CRWWebRequestController and CRWWKNavigationHandler, navigations that are browser-initiated (no opener) and use the TYPED transition are considered trusted. This trust allows them to bypass the security policies that normally prevent arbitrary web content from navigating to app-specific privileged URLs, such as chrome://settings or chrome://inspect.

Potential Attack Scenario

While our analysis is based on code review and we have not executed a live proof-of-concept, the following steps describe a potential attack path:

  1. An attacker crafts a malicious push notification payload containing a privileged URL (e.g., chrome://settings) in the destination field.
  2. The attacker sets the notification’s category to FEEDBACK_IDENTIFIER to ensure it is routed to the ContentNotificationClient.
  3. If the push notification is delivered to a user’s device and the user interacts with it, the browser would invoke the vulnerable code path.
  4. The browser would then initiate a privileged navigation, potentially exposing sensitive internal interfaces or settings to the context of the new tab.

Suggested Fix

Validate the URL scheme in ios/chrome/browser/content_notification/model/content_notification_client.mm before proceeding with the navigation. Access should be restricted to standard web schemes unless a specific internal use case is documented and safe.

if (url.is_empty() || !url.is_valid() || !url.SchemeIsHTTPOrHTTPS()) {
  return true;
}

Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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