Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in Chrome for iOS
DescriptionInsufficient policy enforcement in Chrome for iOS
ComponentChrome for iOS
Bug ClassLogic Error
Tracker502090914
Fix commit27d4ba69c50e (chromium/src) +54/-3
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
ios/chrome/browser/collaboration/model/data_sharing_tab_helper.mm
modified

Files Changed

  • ios/chrome/browser/collaboration/model/data_sharing_tab_helper.mm
  • ios/chrome/browser/tab_switcher/ui_bundled/tab_grid/tab_groups/shared_tab_groups_egtest.mm
From 27d4ba69c50e9891e811179a20e83fae2d09eed7 Mon Sep 17 00:00:00 2001
From: Gauthier Ambard <[email protected]>
Date: Tue, 28 Apr 2026 02:05:34 -0700
Subject: [PATCH] [iOS] Don't open share tab group if not user initiated

The condition was wrong.

NO_IFTTT=Fixing a bug that doesn't exist in the other file

Fixed: 502090914
Change-Id: Ic9bec85c2869dda21f17771c0d938e9820357c5c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7748830
Auto-Submit: Gauthier Ambard <[email protected]>
Commit-Queue: Gauthier Ambard <[email protected]>
Reviewed-by: Louis Romero <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1621616}
---

diff --git a/ios/chrome/browser/collaboration/model/data_sharing_tab_helper.mm b/ios/chrome/browser/collaboration/model/data_sharing_tab_helper.mm
index c893a00..9e2ccfd 100644
--- a/ios/chrome/browser/collaboration/model/data_sharing_tab_helper.mm
+++ b/ios/chrome/browser/collaboration/model/data_sharing_tab_helper.mm
@@ -24,8 +24,10 @@
     return false;
   }
 
-  if (request_info.is_user_initiated && !request_info.user_tapped_recently) {
-    return false;
+  if (!request_info.is_user_initiated &&
+      !PageTransitionCoreTypeIs(request_info.transition_type,
+                                ui::PageTransition::PAGE_TRANSITION_TYPED)) {
+    return request_info.user_tapped_recently;
   }
 
   return true;
diff --git a/ios/chrome/browser/tab_switcher/ui_bundled/tab_grid/tab_groups/shared_tab_groups_egtest.mm b/ios/chrome/browser/tab_switcher/ui_bundled/tab_grid/tab_groups/shared_tab_groups_egtest.mm
index 180ddec..60766714 100644
--- a/ios/chrome/browser/tab_switcher/ui_bundled/tab_grid/tab_groups/shared_tab_groups_egtest.mm
+++ b/ios/chrome/browser/tab_switcher/ui_bundled/tab_grid/tab_groups/shared_tab_groups_egtest.mm
@@ -5,6 +5,7 @@
 #import <Foundation/Foundation.h>
 
 #import "base/feature_list.h"
+#import "base/functional/bind.h"
 #import "base/ios/ios_util.h"
 #import "base/strings/sys_string_conversions.h"
 #import "base/test/ios/wait_util.h"
@@ -37,6 +38,7 @@
 #import "ios/testing/earl_grey/app_launch_manager.h"
 #import "ios/testing/earl_grey/earl_grey_test.h"
 #import "net/test/embedded_test_server/embedded_test_server.h"
+#import "net/test/embedded_test_server/request_handler_util.h"
 #import "ui/base/l10n/l10n_util.h"
 
 using base::test::ios::kWaitForUIElementTimeout;
@@ -190,6 +192,24 @@
                                               timeout:base::Seconds(20)];
 }
 
+// net::EmbeddedTestServer handler that responds with a page that redirects to
+// the URL specified in the query on load.
+std::unique_ptr<net::test_server::HttpResponse> HandleAttackerPage(
+    const net::test_server::HttpRequest& request) {
+  std::unique_ptr<net::test_server::BasicHttpResponse> http_response(
+      new net::test_server::BasicHttpResponse);
+  http_response->set_content_type("text/html");
+  http_response->set_content(
+      "<html><head><script>"
+      "window.onload = function() {"
+      "    location.href = '" +
+      request.GetURL().GetQuery() +
+      "';"
+      "};"
+      "</script></head><body>Attacker Page</body></html>");
+  return std::move(http_response);
+}
+
 }  // namespace
 
 // Test Shared Tab Groups feature (with group creation access).
@@ -205,6 +225,9 @@
 - (void)setUp {
   [super setUp];
   RegisterQueryTitleHandler(self.testServer);
+  self.testServer->RegisterDefaultHandler(base::BindRepeating(
+      net::test_server::HandlePrefixedRequest, "/attacker_page",
+      base::BindRepeating(&HandleAttackerPage)));
   GREYAssertTrue(self.testServer->Start(), @"Test server failed to start");
 
   // Remove the user education screen by default.
@@ -456,6 +479,27 @@
       assertWithMatcher:grey_notVisible()];
 }
 
+// Checks that navigation to a share URL via script (not user initiated) is
+// stopped.
+- (void)testShareURLNavigationStopped {
+  [TabGroupAppInterface mockSharedEntitiesPreview];
+
+  GURL joinGroupURL = data_sharing::GetDataSharingUrl(data_sharing::GroupToken(
+      data_sharing::GroupId("resources%2F3be"), "CggHBicxA_slvx"));
+
+  GURL attackerURL =
+      self.testServer->GetURL("/attacker_page?" + joinGroupURL.spec());
+
+  [ChromeEarlGrey loadURL:attackerURL];
+
+  // Wait for the app to idle.
+  [ChromeEarlGreyUI waitForAppToIdle];
+
+  // Verify that the FakeJoinFlowView does NOT appear.
+  [[EarlGrey selectElementWithMatcher:FakeJoinFlowView()]
+      assertWithMatcher:grey_nil()];
+}
+
 // Checks that the IPH is presented when the user foreground the app with a
 // shared tab group active.
 // TODO(crbug.com/411064928): Re-enable this test.
@@ -1693,7 +1737,12 @@
   [TabGroupAppInterface mockSharedEntitiesPreview];
   GURL joinGroupURL = data_sharing::GetDataSharingUrl(data_sharing::GroupToken(
       data_sharing::GroupId("resources%2F3be"), "CggHBicxA_slvx"));
-  [ChromeEarlGrey loadURL:joinGroupURL waitForCompletion:NO];
+
+  std::string pageContent =
+      "data:text/html,<html><body><a id='join-link' href='" +
+      joinGroupURL.spec() + "'>Join</a></body></html>";
+  [ChromeEarlGrey loadURL:GURL(pageContent)];
+  [ChromeEarlGrey tapWebStateElementWithID:@"join-link"];
 
   WaitForFakeJoinFlowView();
 
Loading diff…

Original Bug Report

reported by [email protected]

Potential iOS DataSharingTabHelper gesture gate bypass

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.

Overview: A logic error in DataSharingTabHelper on iOS allows script-initiated navigations to bypass user-gesture requirements for the Shared Tab Group join flow. This enables a malicious site to automatically trigger the join flow, dismissing active modals and presenting a trusted system sheet without user interaction.

Affected files:

  • ios/chrome/browser/collaboration/model/data_sharing_tab_helper.mm
  • ios/chrome/browser/data_sharing/model/data_sharing_ui_delegate_ios.mm
  • ios/web/navigation/crw_wk_navigation_handler.mm
  • ios/web/web_state/user_interaction_state.mm

Estimated timestamp from git blame: 2025-04-30

Summary

The DataSharingTabHelper on iOS contains a logic error in its gesture gating mechanism, allowing script-initiated navigations to bypass intended user-gesture requirements when joining shared tab groups. A malicious website can exploit this to automatically trigger the ‘Join shared tab group’ flow, which aggressively dismisses current modal dialogs (including security warnings or settings) and presents a trusted system sheet to the user without any prior interaction.

Vulnerability Details

In ios/chrome/browser/collaboration/model/data_sharing_tab_helper.mm, the function ShouldHandleShareURLNavigation determines whether a navigation to a Data Sharing URL should be intercepted to show the native “Join Group” UI. The implementation contains the following gate:

if (request_info.is_user_initiated && !request_info.user_tapped_recently) {
  return false;
}

On iOS, request_info.is_user_initiated and request_info.user_tapped_recently are populated by the crw_wk_navigation_handler.mm delegate based on the UserInteractionState class.

Crucially, UserInteractionState::IsUserInteracting() strictly returns false if HasUserTappedRecently() is false. Because of this dependency, it is logically impossible for is_user_initiated to be true while user_tapped_recently is false in the context of an automated script execution.

When a site triggers an automated, script-initiated navigation (e.g., via window.location.href = data_sharing_url;), there is no recent tap. Consequently, is_user_initiated evaluates to false.

Because the if condition uses a logical &&, the entire block short-circuits and is bypassed. The function erroneously proceeds to the end and returns true, authorizing the interception. This behavior is contrary to the Desktop and Android implementations (e.g., chrome/browser/data_sharing/data_sharing_navigation_throttle.cc), which correctly verify that renderer-initiated navigations are accompanied by a user gesture.

Once the request is authorized, the interception eventually routes to DataSharingUIDelegateIOS::HandleShareURLIntercepted, which executes dismissModalDialogsWithCompletion: to aggressively clear any active top-level UI, followed by presenting the native iOS ShareKit join sheet.

Impact

An attacker can trigger this behavior by navigating the top-level frame to a Data Sharing URL without user interaction. This allows an attacker to:

  1. Forcefully dismiss active Chrome modal dialogs (such as password prompts, permission requests, or security warnings) without user consent.
  2. Present a trusted, native system sheet to the user without interaction.
  3. If the user subsequently clicks ‘Join’ on the unexpected prompt, the attacker gains access to the user’s Google identity (name and email) via the group roster and can push arbitrary HTTP/HTTPS tabs to the user’s browser indefinitely via the Tab Group Sync service.

Potential Reproduction Steps

Note: These are potential steps based on static analysis.

  1. Using Chrome on iOS, ensure you are signed in to a Google account and that the Shared Tab Groups feature is enabled.
  2. Visit a malicious website that executes a script-initiated navigation to a shared tab group URL on load:
    window.onload = function() {
        location.href = 'https://www.google.com/chrome/tabshare/?g=ATTACKER_GROUP&t=ATTACKER_TOKEN';
    };
    
  3. Observe that any active modal dialogs are dismissed and the ‘Join shared tab group’ native sheet is presented immediately without any user interaction.

Suggested Fix

Correct the logic in ShouldHandleShareURLNavigation to explicitly reject automated renderer navigations. The logic should require a user gesture if the navigation is renderer-initiated (or in the iOS context, if the request lacks a recent tap), similar to the implementation in DataSharingNavigationThrottle::ShouldHandleShareURLNavigation.

For example:

if (!request_info.is_user_initiated && !request_info.user_tapped_recently) {
  return false;
}

(Or alternatively, checking if a tap occurred recently regardless of the is_user_initiated heuristic).

Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b


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