Chrome · TrustedWebActivities
CVE-2026-87552
Logic Error in TrustedWebActivities
Overview
High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/android/java/src/org/chromium/chrome/browser/browserservices/trustedwebactivityui/sharing/TwaSharingController.java |
modified | |
ifchrome/android/java/src/org/chromium/chrome/browser/customtabs/content/WebAppLaunchHandler.java |
modified |
Files Changed
chrome/android/java/src/org/chromium/chrome/browser/LaunchIntentDispatcher.javachrome/android/java/src/org/chromium/chrome/browser/browserservices/trustedwebactivityui/sharing/TwaSharingController.javachrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabIntentDataProvider.javachrome/android/java/src/org/chromium/chrome/browser/customtabs/content/WebAppLaunchHandler.java
Patch
From 0589fb4dd47c16583b13a8f939d2f472b36888c1 Mon Sep 17 00:00:00 2001
From: Kunjan Vinay Patel <ksukh@google.com>
Date: Wed, 26 Aug 2026 09:49:58 -0700
Subject: [PATCH] Verify caller URI permissions before Web Share Target in TWA
Unprivileged Android apps could drive Trusted Web Activity (TWA) Web
Share Target into uploading content URIs that only Chrome has permission
to read (such as Chrome private data or MediaStore downloads) to an
attacker-controlled HTTPS domain without the caller having read
permissions for those URIs.
Changes Include:
- Validates shared content URIs in the trampoline activity
(LaunchIntentDispatcher) using WebAppLaunchHandler to verify caller read permissions (via ComponentCaller on Android 15+ or session UID checks).
- Stashes verified URIs in a new EXTRA_VERIFIED_SHARE_DATA bundle and
strips any pre-existing/spoofed verified share data from incoming intents during the dispatch process in WebAppLaunchHandler.
- Updates CustomTabIntentDataProvider#getShareData to use safeGetBundleExtra to correctly unparcel ShareData from the intent.
- Adds filtering in TwaSharingController to verify URIs before dispatching to the web page via GET or POST, ensuring only authorized URIs are shared.
- Implements filterShareData and copyShareDataPermissions in WebAppLaunchHandler to provide centralized URI permission verification using session-based UID/PID or ComponentCaller.
- Adds Robolectric unit tests for LaunchIntentDispatcher verifying share target permission handling (including partial filtering and invalid URI rejection), for CustomTabIntentDataProvider verifying share data unparcelling, and for WebAppLaunchHandler verifying the filtering logic.
Fixed: 548571656, 547426657
Change-Id: Icd2710e38be5c074a31a68aa6a7e66dfbe24004d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8280393
Reviewed-by: Sirisha Kavuluru <skavuluru@google.com>
Commit-Queue: Kunjan Patel <ksukh@chromium.org>
Reviewed-by: Jinsuk Kim <jinsukkim@chromium.org>
Reviewed-by: Daniel Murphy <dmurph@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1686504}
---
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/LaunchIntentDispatcher.java b/chrome/android/java/src/org/chromium/chrome/browser/LaunchIntentDispatcher.java
index b1eb69a5..374d1ef 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/LaunchIntentDispatcher.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/LaunchIntentDispatcher.java
@@ -320,6 +320,7 @@
Intent launchIntent = createCustomTabActivityIntent(mActivity, intent);
WebAppLaunchHandler.copyFilePermissions(mActivity, intent, launchIntent);
+ WebAppLaunchHandler.copyShareDataPermissions(mActivity, intent, launchIntent);
Uri extraReferrer = mActivity.getReferrer();
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/browserservices/trustedwebactivityui/sharing/TwaSharingController.java b/chrome/android/java/src/org/chromium/chrome/browser/browserservices/trustedwebactivityui/sharing/TwaSharingController.java
index e67c46b5..d438f3b 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/browserservices/trustedwebactivityui/sharing/TwaSharingController.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/browserservices/trustedwebactivityui/sharing/TwaSharingController.java
@@ -6,6 +6,7 @@
import static org.chromium.build.NullUtil.assertNonNull;
+import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.text.TextUtils;
@@ -23,6 +24,7 @@
import org.chromium.chrome.browser.browserservices.ui.controller.Verifier;
import org.chromium.chrome.browser.customtabs.content.CustomTabActivityNavigationController;
import org.chromium.chrome.browser.customtabs.content.CustomTabActivityTabProvider;
+import org.chromium.chrome.browser.customtabs.content.WebAppLaunchHandler;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.webapps.WebApkPostShareTargetNavigator;
import org.chromium.content_public.browser.LoadUrlParams;
@@ -67,6 +69,24 @@
Intent intent = intentDataProvider.getIntent();
assert intent != null;
+ if (shareData.uris != null && !shareData.uris.isEmpty()) {
+ Tab tab = mTabProvider.getTab();
+ Activity activity =
+ (tab != null && tab.getWindowAndroid() != null)
+ ? tab.getWindowAndroid().getActivity().get()
+ : null;
+ // Pass null for caller because activity.getInitialCaller() in CustomTabActivity
+ // would return Chrome's own UID (due to trampoline dispatch). Passing null
+ // forces fallback to session UID/PID verification.
+ shareData =
+ WebAppLaunchHandler.filterShareData(
+ intentDataProvider, activity, /* caller= */ null);
+ if (shareData == null) {
+ return Promise.fulfilled(false);
+ }
+ }
+
+ final ShareData finalShareData = shareData;
return mVerifierDelegate
.verify(shareTarget.getAction())
.then(
@@ -76,13 +96,13 @@
return false;
}
if (shareTarget.isShareMethodPost()) {
- return sendPost(shareData, shareTarget);
+ return sendPost(finalShareData, shareTarget);
}
mNavigationController.navigate(
new LoadUrlParams(
computeStartUrlForGETShareTarget(
- shareData, shareTarget)),
+ finalShareData, shareTarget)),
intent);
return true;
});
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabIntentDataProvider.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabIntentDataProvider.java
index 5cfbc73..a97b9c901 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabIntentDataProvider.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabIntentDataProvider.java
@@ -269,6 +269,10 @@
public static final String EXTRA_VERIFIED_FILE_CAN_WRITE =
"org.chromium.chrome.browser.customtabs.EXTRA_VERIFIED_FILE_CAN_WRITE";
+ /** Extra that contains the verified ShareData bundle. */
+ public static final String EXTRA_VERIFIED_SHARE_DATA =
+ "org.chromium.chrome.browser.customtabs.EXTRA_VERIFIED_SHARE_DATA";
+
/**
* Extra that, if set, makes the Custom Tab Activity's height to be x pixels, the Custom Tab
* will behave as a bottom sheet. x will be clamped between 50% and 100% of screen height.
@@ -1683,7 +1687,7 @@
@Override
public @Nullable ShareData getShareData() {
Bundle bundle =
- IntentUtils.safeGetParcelableExtra(
+ IntentUtils.safeGetBundleExtra(
getIntent(), TrustedWebActivityIntentBuilder.EXTRA_SHARE_DATA);
if (bundle == null) return null;
try {
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/content/WebAppLaunchHandler.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/content/WebAppLaunchHandler.java
index 14b2f57..e1742af 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/content/WebAppLaunchHandler.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/content/WebAppLaunchHandler.java
@@ -9,6 +9,7 @@
import static androidx.browser.trusted.LaunchHandlerClientMode.NAVIGATE_EXISTING;
import static androidx.browser.trusted.LaunchHandlerClientMode.NAVIGATE_NEW;
import static androidx.browser.trusted.TrustedWebActivityIntentBuilder.EXTRA_FILE_HANDLING_DATA;
+import static androidx.browser.trusted.TrustedWebActivityIntentBuilder.EXTRA_SHARE_DATA;
import static org.chromium.build.NullUtil.assertNonNull;
import static org.chromium.build.NullUtil.assumeNonNull;
@@ -28,6 +29,7 @@
import androidx.browser.trusted.FileHandlingData;
import androidx.browser.trusted.LaunchHandlerClientMode.ClientMode;
+import androidx.browser.trusted.sharing.ShareData;
import org.jni_zero.JNINamespace;
import org.jni_zero.JniType;
@@ -143,7 +145,7 @@
return true;
}
- private static boolean isValidLaunchUri(Uri uri) {
+ public static boolean isValidLaunchUri(Uri uri) {
if (uri == null) return false;
// Only content URIs are allowed. Legitimate file launching on Android should
@@ -654,6 +656,127 @@
CustomTabIntentDataProvider.EXTRA_VERIFIED_FILE_CAN_WRITE, canWriteArray);
}
+ /**
+ * Checks caller permissions for any file URIs in sourceIntent's share data and stashes the
+ * verified results in targetIntent.
+ *
+ * @param activity The launcher activity.
+ * @param sourceIntent The incoming intent containing client extras.
+ * @param targetIntent The launch intent being prepared for CustomTabActivity.
+ */
+ public static void copyShareDataPermissions(
+ Activity activity, Intent sourceIntent, Intent targetIntent) {
+ // Strip EXTRA_VERIFIED_SHARE_DATA if present on targetIntent so that it cannot be spoofed
+ // by CCT client apps.
+ IntentUtils.safeRemoveExtra(
+ targetIntent, CustomTabIntentDataProvider.EXTRA_VERIFIED_SHARE_DATA);
+
+ Bundle shareDataBundle = IntentUtils.safeGetBundleExtra(sourceIntent, EXTRA_SHARE_DATA);
+ if (shareDataBundle == null) {
+ return;
+ }
+
+ ShareData shareData;
+ try {
+ shareData = ShareData.fromBundle(shareDataBundle);
+ } catch (Throwable e) {
+ Log.w(TAG, "Failed to parse share data", e);
+ return;
+ }
+ if (shareData == null) {
+ return;
+ }
+ if (shareData.uris == null || shareData.uris.isEmpty()) {
+ targetIntent.putExtra(
+ CustomTabIntentDataProvider.EXTRA_VERIFIED_SHARE_DATA, shareData.toBundle());
+ return;
+ }
+
+ Object caller = null;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ try {
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.
References
On This Page