CVE-2026-13927
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/android/java/src/org/chromium/chrome/browser/IntentHandler.java |
modified |
Files Changed
chrome/android/java/src/org/chromium/chrome/browser/IntentHandler.javachrome/android/javatests/src/org/chromium/chrome/browser/ChromeTabbedActivityTest.javachrome/android/junit/src/org/chromium/chrome/browser/IntentHandlerRobolectricTest.java
Patch
From fb7b1cd2fc28f99edef184812c3778ba5ad8534a Mon Sep 17 00:00:00 2001 From: Zhe Li <[email protected]> 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 <[email protected]> Commit-Queue: Zhe Li <[email protected]> Reviewed-by: Calder Kitagawa <[email protected]> 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
Original Bug Report
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.javachrome/android/java/src/org/chromium/chrome/browser/IntentHandler.javachrome/browser/tabmodel/android/java/src/org/chromium/chrome/browser/tabmodel/TabGroupModelFilter.javachrome/browser/tabmodel/android/java/src/org/chromium/chrome/browser/tabmodel/TabGroupMetadata.javachrome/browser/tabmodel/android/java/src/org/chromium/chrome/browser/tabmodel/TabGroupUtils.javachrome/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.
- Missing Intent Sender Verification: In
IntentHandler.shouldIgnoreIntent()(IntentHandler.java:994-998), the logic for handling intents containingTabGroupMetadataonly checkswasIntentSenderChrome()(viaisAllowedIncognitoIntent()) if theisIncognitoflag in the metadata is true. If an attacker setsisIncognitoto 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. - Reaching Privileged APIs: Once accepted,
ChromeTabbedActivityprocesses the intent viamaybeHandleGroupUrlsIntent(). This method loads the URLs and then callsTabGroupUtils.regroupTabs(). This eventually invokesTabGroupModelFilter.createTabGroupForTabGroupSync()using the attacker-controlled 128-bitTokenextracted from the intent metadata. This API is explicitly documented as an internal-only interface intended only for the tab group sync service. - Sync Abuse: By successfully invoking this privileged API, the attacker creates a new local tab group with their chosen Token, title, and color. The
TabGroupSyncLocalObserverdetects this new group (e.g., via thedidMergeTabToGroupcallback) and pushes it to theTabGroupSyncService. This forces Chrome to synchronize the malicious group across the victim’s signed-in devices. - Deserialization Surface: As an additional note,
TabGroupMetadata.maybeCreateFromBundle()usesBundle.getSerializable()to extract thetabIdsToUrlsarray (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.)
- A malicious application on the device creates an
Intenttargetingorg.chromium.chrome.browser.ChromeTabbedActivity. - The attacker adds a
Bundleextra using the keyIntentHandler.EXTRA_TAB_GROUP_METADATA. - Inside the bundle, the attacker sets
KEY_IS_INCOGNITOtofalse. They also provide a customTokenforKEY_TAB_GROUP_ID, anArrayListof target URLs forKEY_TAB_IDS_TO_URLS, and an arbitrary title and color. - The attacker launches the intent. Assuming the device is unlocked and the screen is on (passing
shouldIgnoreIntentUrl()checks), Chrome processes the intent. - Chrome creates the tab group and syncs the attacker’s chosen URLs to the user’s other devices.
Suggested Fix
- Enforce Sender Verification: In
IntentHandler.shouldIgnoreIntent(), enforcewasIntentSenderChrome(intent)for all intents containingEXTRA_TAB_GROUP_METADATA, regardless of theisIncognitoflag. External apps should not be able to trigger tab group drag-and-drop or regrouping operations. - Remove getSerializable: Refactor
TabGroupMetadatato pass the URLs in a safer manner (e.g., usingBundle.putStringArrayListor Parcelable arrays) to remove thegetSerializable()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.