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 UI
DescriptionInsufficient validation of untrusted input in UI
ComponentUI
Bug ClassLogic Error
Tracker511826446
Fix commitfb7b1cd2fc28 (chromium/src) +109/-26
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
if
chrome/android/java/src/org/chromium/chrome/browser/IntentHandler.java
modified

Files Changed

  • chrome/android/java/src/org/chromium/chrome/browser/IntentHandler.java
  • chrome/android/javatests/src/org/chromium/chrome/browser/ChromeTabbedActivityTest.java
  • chrome/android/junit/src/org/chromium/chrome/browser/IntentHandlerRobolectricTest.java
From fb7b1cd2fc28f99edef184812c3778ba5ad8534a Mon Sep 17 00:00:00 2001
From: Zhe Li <zheliooo@google.com>
Date: Thu, 21 May 2026 10:52:38 -0700
Subject: [PATCH] Check wasIntentSenderChrome for tab group and multi-tabs drag drop

Fixed: 511826446
Change-Id: I1849f66fcf9471fb9066772daad165d2b55ebe55
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7837461
Reviewed-by: Madhav Pruthi <madhavpruthi@google.com>
Commit-Queue: Zhe Li <zheliooo@google.com>
Reviewed-by: Calder Kitagawa <ckitagawa@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1634401}
---

diff --git a/chrome/android/java/src/org/chromium/chrome/browser/IntentHandler.java b/chrome/android/java/src/org/chromium/chrome/browser/IntentHandler.java
index 2ebafc2..cc46028 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/IntentHandler.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/IntentHandler.java
@@ -963,12 +963,9 @@
             // If the intent contains a list of tabs to reparent, it's a valid intent from Chrome.
             @Nullable MultiTabMetadata multiTabMetadata = getMultiTabMetadata(intent);
             if (multiTabMetadata != null) {
-                // Exit early if the incognito intent is not allowed.
-                if (IntentUtils.safeGetBooleanExtra(intent, EXTRA_OPEN_NEW_INCOGNITO_TAB, false)
-                        && !isAllowedIncognitoIntent(
-                                wasIntentSenderChrome(intent), isCustomTab, intent)) {
-                    return true;
-                }
+                // Multi-tab metadata intents should only be from Chrome.
+                if (!wasIntentSenderChrome(intent)) return true;
+
                 ArrayList<Integer> tabIds = multiTabMetadata.tabIds;
                 ArrayList<String> urls = multiTabMetadata.urls;
 
@@ -990,12 +987,8 @@
             // Ignore all invalid URLs, regardless of what the intent was.
             @Nullable TabGroupMetadata tabGroupMetadata = IntentHandler.getTabGroupMetadata(intent);
             if (tabGroupMetadata != null) {
-                // Exit early if the incognito intent is not allowed.
-                if (tabGroupMetadata.isIncognito
-                        && !isAllowedIncognitoIntent(
-                                wasIntentSenderChrome(intent), isCustomTab, intent)) {
-                    return true;
-                }
+                // Tab group metadata intents should only be from Chrome.
+                if (!wasIntentSenderChrome(intent)) return true;
 
                 // Check url validity and remove invalid urls if needed.
                 List<Entry<Integer, String>> tabIdsToUrls = tabGroupMetadata.tabIdsToUrls;
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/ChromeTabbedActivityTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/ChromeTabbedActivityTest.java
index aaa25ac5..3b9cfa27 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/ChromeTabbedActivityTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/ChromeTabbedActivityTest.java
@@ -668,13 +668,9 @@
         intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
         intent.setClass(mActivity, ChromeTabbedActivity.class);
         IntentHandler.setTabGroupMetadata(intent, createTabGroupMetadata());
+        IntentUtils.setForceIsTrustedIntentForTesting(true);
 
-        // The newly created ChromeTabbedActivity (created via #startActivity()) should be
-        // destroyed, and the intent should be launched in the existing ChromeTabbedActivity.
-        ApplicationTestUtils.waitForActivityWithClass(
-                ChromeTabbedActivity.class,
-                Stage.DESTROYED,
-                () -> mActivity.getApplicationContext().startActivity(intent));
+        ThreadUtils.runOnUiThreadBlocking(() -> mActivity.onNewIntent(intent));
 
         Assert.assertEquals(
                 "No new window should be opened.",
@@ -700,8 +696,7 @@
                             tabModel.getTabAt(3).getUrl().getSpec(),
                             Matchers.equalTo(TAB_IDS_TO_URLS.get(0).getValue()));
 
-                    // Verify the tabs are grouped with the correct rootId and tabGroupId.
-                    int expectedRootId = tabModel.getTabAt(1).getId();
+                    // Verify the tabs are grouped with the correct tabGroupId.
                     for (int i = 1; i < tabModel.getCount() - 1; i++) {
                         Tab curTab = tabModel.getTabAt(i);
                         Assert.assertEquals(
@@ -730,6 +725,7 @@
         Intent reparentingIntent = new Intent(Intent.ACTION_VIEW);
         reparentingIntent.setClass(mActivity, ChromeTabbedActivity.class);
         reparentingIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        IntentUtils.setForceIsTrustedIntentForTesting(true);
 
         IntentHandler.setMultiTabMetadata(
                 reparentingIntent,
@@ -888,6 +884,7 @@
         Intent reparentingIntent = new Intent(Intent.ACTION_VIEW);
         reparentingIntent.setClass(mActivity, ChromeTabbedActivity.class);
         reparentingIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        IntentUtils.setForceIsTrustedIntentForTesting(true);
 
         IntentHandler.setMultiTabMetadata(
                 reparentingIntent,
@@ -906,10 +903,10 @@
                 () -> {
                     TabModel tabModel = mActivity.getCurrentTabModel();
                     Criteria.checkThat(tabModel.getCount(), Matchers.is(initialTabCount.get() + 2));
-                    // Tabs are added at the end of the tab model.
-                    // Pinned tab is added to the start.
+                    // A multi-tab intent containing a pinned tab forces all tabs to move to the
+                    // front.
                     Tab firstTab = tabModel.getTabAt(initialTabCount.get() - 1);
-                    Tab secondTab = tabModel.getTabAt(initialTabCount.get() + 1);
+                    Tab secondTab = tabModel.getTabAt(initialTabCount.get());
                     Criteria.checkThat(firstTab.getUrl(), Matchers.is(JUnitTestGURLs.URL_1));
                     Criteria.checkThat(secondTab.getUrl(), Matchers.is(JUnitTestGURLs.URL_2));
                     Criteria.checkThat(firstTab.getIsPinned(), Matchers.is(true));
@@ -928,6 +925,7 @@
         Intent dragIntent = new Intent(Intent.ACTION_VIEW);
         dragIntent.setClass(mActivity, ChromeTabbedActivity.class);
         dragIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        IntentUtils.setForceIsTrustedIntentForTesting(true);
 
         IntentHandler.setMultiTabMetadata(
                 dragIntent,
@@ -967,6 +965,7 @@
         Intent dragIntent = new Intent(Intent.ACTION_VIEW);
         dragIntent.setClass(mActivity, ChromeTabbedActivity.class);
         dragIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        IntentUtils.setForceIsTrustedIntentForTesting(true);
 
         IntentHandler.setMultiTabMetadata(
                 dragIntent,
@@ -985,10 +984,10 @@
                 () -> {
                     TabModel tabModel = mActivity.getCurrentTabModel();
                     Criteria.checkThat(tabModel.getCount(), Matchers.is(initialTabCount.get() + 2));
-                    // Tabs are added at the end of the tab model.
-                    // Pinned tab is added to the start.
+                    // A multi-tab intent containing a pinned tab forces all tabs to move to the
+                    // front.
                     Tab firstTab = tabModel.getTabAt(initialTabCount.get() - 1);
-                    Tab secondTab = tabModel.getTabAt(initialTabCount.get() + 1);
+                    Tab secondTab = tabModel.getTabAt(initialTabCount.get());
                     Criteria.checkThat(firstTab.getUrl(), Matchers.is(JUnitTestGURLs.URL_1));
                     Criteria.checkThat(secondTab.getUrl(), Matchers.is(JUnitTestGURLs.URL_2));
                     Criteria.checkThat(firstTab.getIsPinned(), Matchers.is(true));
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/IntentHandlerRobolectricTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/IntentHandlerRobolectricTest.java
index af715dfc..90f98b3 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/IntentHandlerRobolectricTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/IntentHandlerRobolectricTest.java
@@ -50,6 +50,7 @@
 
 import org.chromium.base.ContextUtils;
 import org.chromium.base.IntentUtils;
+import org.chromium.base.Token;
 import org.chromium.base.library_loader.LibraryLoader;
 import org.chromium.base.test.BaseRobolectricTestRunner;
 import org.chromium.base.test.util.Feature;
@@ -63,6 +64,8 @@
 import org.chromium.chrome.browser.flags.ChromeFeatureList;
 import org.chromium.chrome.browser.tab.Tab;
 import org.chromium.chrome.browser.tabmodel.AsyncTabCreationParams;
+import org.chromium.chrome.browser.tabmodel.MultiTabMetadata;
+import org.chromium.chrome.browser.tabmodel.TabGroupMetadata;
 import org.chromium.chrome.browser.webapps.WebappLauncherActivity;
 import org.chromium.chrome.test.util.browser.webapps.WebappTestHelper;
 import org.chromium.components.external_intents.ExternalNavigationHandler;
@@ -70,8 +73,10 @@
 import org.chromium.content_public.common.Referrer;
 import org.chromium.url.GURL;
 
+import java.util.AbstractMap.SimpleImmutableEntry;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 
 /**
  * Robolectric tests for IntentHandler. These tests do not require use of the native library (other
@@ -823,4 +828,90 @@
         LoadUrlParams params = IntentHandler.createLoadUrlParamsForIntent(GOOGLE_URL, intent, 0);
         Assert.assertNull(params.getInternalScrollToTextFragment());
     }
+
+    @Test
+    @SmallTest
+    public void testShouldIgnoreIntent_TabGroupMetadata() {
+        // Trusted source should be allowed.
+        Intent trustedIntent =
+                createTabGroupIntent(/* isIncognito= */ false, /* isTrusted= */ true);
+        Assert.assertFalse(IntentHandler.shouldIgnoreIntent(trustedIntent, null));
+
+        // Untrusted source should be ignored.
+        Intent untrustedIntent =
+                createTabGroupIntent(/* isIncognito= */ false, /* isTrusted= */ false);
+        Assert.assertTrue(IntentHandler.shouldIgnoreIntent(untrustedIntent, null));
+
+        // Untrusted incognito should also be ignored.
+        Intent untrustedIncognito =
+                createTabGroupIntent(/* isIncognito= */ true, /* isTrusted= */ false);
+        Assert.assertTrue(IntentHandler.shouldIgnoreIntent(untrustedIncognito, null));
+
+        // Untrusted Custom Tab should also be ignored.
+        Assert.assertTrue(
+                IntentHandler.shouldIgnoreIntent(untrustedIntent, /* isCustomTab= */ true));
+    }
+
+    @Test
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential logic flaw allows local intent to bypass intent sender checks and create synced tab groups

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 https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A malicious Android application can bypass intent sender verification in ChromeTabbedActivity by sending a crafted intent containing non-incognito tab group metadata. This allows the attacker to invoke privileged internal APIs to create a new tab group with an arbitrary Token. The new group is then automatically synchronized to the user’s other signed-in devices via Chrome’s tab group sync service.

Affected files:

  • chrome/android/java/src/org/chromium/chrome/browser/ChromeTabbedActivity.java
  • chrome/android/java/src/org/chromium/chrome/browser/IntentHandler.java
  • chrome/browser/tabmodel/android/java/src/org/chromium/chrome/browser/tabmodel/TabGroupModelFilter.java
  • chrome/browser/tabmodel/android/java/src/org/chromium/chrome/browser/tabmodel/TabGroupMetadata.java
  • chrome/browser/tabmodel/android/java/src/org/chromium/chrome/browser/tabmodel/TabGroupUtils.java
  • chrome/browser/tab_group_sync/android/java/src/org/chromium/chrome/browser/tab_group_sync/TabGroupSyncLocalObserver.java

Estimated timestamp from git blame: 2025-05-09

Summary

ChromeTabbedActivity is an exported Android activity that processes intents for various browser actions. A potential logic flaw exists where intents carrying tab group metadata (via EXTRA_TAB_GROUP_METADATA) are not properly verified to originate from a trusted source (i.e., Chrome itself) if the group is marked as non-incognito. This allows a malicious local application to trigger the creation of a new tab group with attacker-controlled properties, which then propagates to the user’s other signed-in devices through Chrome’s sync service.

Vulnerability Details

The vulnerability resides in the interaction between IntentHandler.shouldIgnoreIntent() and ChromeTabbedActivity when processing tab group metadata.

  1. Missing Intent Sender Verification: In IntentHandler.shouldIgnoreIntent() (IntentHandler.java:994-998), the logic for handling intents containing TabGroupMetadata only checks wasIntentSenderChrome() (via isAllowedIncognitoIntent()) if the isIncognito flag in the metadata is true. If an attacker sets isIncognito to false, this crucial sender verification is skipped. For standard tab groups, it proceeds to validate only the URLs within the metadata. If the URLs are valid HTTP(S) links, the intent is accepted regardless of the sender.
  2. Reaching Privileged APIs: Once accepted, ChromeTabbedActivity processes the intent via maybeHandleGroupUrlsIntent(). This method loads the URLs and then calls TabGroupUtils.regroupTabs(). This eventually invokes TabGroupModelFilter.createTabGroupForTabGroupSync() using the attacker-controlled 128-bit Token extracted from the intent metadata. This API is explicitly documented as an internal-only interface intended only for the tab group sync service.
  3. Sync Abuse: By successfully invoking this privileged API, the attacker creates a new local tab group with their chosen Token, title, and color. The TabGroupSyncLocalObserver detects this new group (e.g., via the didMergeTabToGroup callback) and pushes it to the TabGroupSyncService. This forces Chrome to synchronize the malicious group across the victim’s signed-in devices.
  4. Deserialization Surface: As an additional note, TabGroupMetadata.maybeCreateFromBundle() uses Bundle.getSerializable() to extract the tabIdsToUrls array (TabGroupMetadata.java:128). This exposes a deserialization surface in the browser process to untrusted local intents.

Potential Attack Steps

(Note: These are potential steps based on code analysis; a working proof-of-concept has not been executed by our tooling.)

  1. A malicious application on the device creates an Intent targeting org.chromium.chrome.browser.ChromeTabbedActivity.
  2. The attacker adds a Bundle extra using the key IntentHandler.EXTRA_TAB_GROUP_METADATA.
  3. Inside the bundle, the attacker sets KEY_IS_INCOGNITO to false. They also provide a custom Token for KEY_TAB_GROUP_ID, an ArrayList of target URLs for KEY_TAB_IDS_TO_URLS, and an arbitrary title and color.
  4. The attacker launches the intent. Assuming the device is unlocked and the screen is on (passing shouldIgnoreIntentUrl() checks), Chrome processes the intent.
  5. Chrome creates the tab group and syncs the attacker’s chosen URLs to the user’s other devices.

Suggested Fix

  1. Enforce Sender Verification: In IntentHandler.shouldIgnoreIntent(), enforce wasIntentSenderChrome(intent) for all intents containing EXTRA_TAB_GROUP_METADATA, regardless of the isIncognito flag. External apps should not be able to trigger tab group drag-and-drop or regrouping operations.
  2. Remove getSerializable: Refactor TabGroupMetadata to pass the URLs in a safer manner (e.g., using Bundle.putStringArrayList or Parcelable arrays) to remove the getSerializable() call and close the potential deserialization surface.

Evaluated with Chrome root at commit: eca8648a4e1cdfdda68c495a6003059fed641955


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