CVE-2026-13863
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forchrome/android/java/src/org/chromium/chrome/browser/customtabs/content/WebAppLaunchHandler.java |
modified |
Files Changed
chrome/android/java/src/org/chromium/chrome/browser/customtabs/content/WebAppLaunchHandler.javachrome/android/junit/src/org/chromium/chrome/browser/customtabs/content/WebAppLaunchHandlerTest.java
Patch
From f98f0f4e339caeadfee03ffacdb1f1473b75b38c Mon Sep 17 00:00:00 2001
From: Nate Chapin <japhet@chromium.org>
Date: Fri, 15 May 2026 16:59:27 -0700
Subject: [PATCH] Remove grantUriPermission() permission call in WebAppLaunchHandler.launchNewIntent()
The package should already have the permissions it needs.
Fixed: 496012495
Change-Id: I8a757f148d8823be348b2a530f8373e999129775
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7779798
Reviewed-by: Jinsuk Kim <jinsukkim@chromium.org>
Commit-Queue: Nate Chapin <japhet@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1631646}
---
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 c896c8e..6e1d5f06 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
@@ -260,13 +260,6 @@
/* This method can be called for file handling intent as well. In this case we need to send
a file data extras as well. Also we need to grant file permissions */
if (fileData != null && !fileData.uris.isEmpty()) {
- for (Uri uri : fileData.uris) {
- mActivity.grantUriPermission(
- packageName,
- uri,
- Intent.FLAG_GRANT_READ_URI_PERMISSION
- | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
- }
newIntent.putExtra(EXTRA_FILE_HANDLING_DATA, fileData.toBundle());
}
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/content/WebAppLaunchHandlerTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/content/WebAppLaunchHandlerTest.java
index 5030e78..e9f8d83 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/content/WebAppLaunchHandlerTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/content/WebAppLaunchHandlerTest.java
@@ -13,6 +13,7 @@
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
@@ -21,6 +22,7 @@
import android.app.Activity;
import android.content.Intent;
+import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Looper;
@@ -372,12 +374,36 @@
mExpectedFileList = new String[] {CONTENT_URI};
doTestNavigateNewNewIntent(
LaunchHandlerClientMode.NAVIGATE_NEW, /* expectedStartActivityTimes= */ 1);
- verify(mActivityMock, times(1))
+ verify(mActivityMock, never())
.grantUriPermission(
eq(TEST_PACKAGE_NAME), eq(mFileHandlingData.uris.get(0)), anyInt());
}
@Test
+ public void navigateNewStartNewTask_fileData_chromePrivateData() {
+ doTestNavigateNewInitialIntent(LaunchHandlerClientMode.NAVIGATE_NEW);
+
+ final Uri pwCsv =
+ Uri.parse(
+ "content://com.android.chrome.FileProvider/passwords/"
+ + "Chrome%20Passwords.csv");
+
+ when(mActivityMock.checkCallingUriPermission(eq(pwCsv), anyInt()))
+ .thenReturn(PackageManager.PERMISSION_DENIED);
+
+ mFileHandlingData = new FileHandlingData(Arrays.asList(Uri.parse(CONTENT_URI), pwCsv));
+ mExpectedFileList = new String[] {CONTENT_URI};
+ doTestNavigateNewNewIntent(
+ LaunchHandlerClientMode.NAVIGATE_NEW, /* expectedStartActivityTimes= */ 1);
+ verify(mActivityMock, never())
+ .grantUriPermission(
+ eq(TEST_PACKAGE_NAME), eq(mFileHandlingData.uris.get(0)), anyInt());
+ verify(mActivityMock, never())
+ .grantUriPermission(
+ eq(TEST_PACKAGE_NAME), eq(mFileHandlingData.uris.get(1)), anyInt());
+ }
+
+ @Test
public void currentPageVerifierFailed() {
when(mCurrentPageVerifierMock.getState())
.thenReturn(
@@ -406,7 +432,7 @@
mExpectedFileList = new String[] {CONTENT_URI};
doTestNavigateNewNewIntent(
LaunchHandlerClientMode.NAVIGATE_EXISTING, /* expectedStartActivityTimes= */ 1);
- verify(mActivityMock, times(1))
+ verify(mActivityMock, never())
.grantUriPermission(
eq(TEST_PACKAGE_NAME), eq(mFileHandlingData.uris.get(0)), anyInt());
}
Original Bug Report
Confused deputy in WebAppLaunchHandler grants attacker READ/WRITE access to Chrome's internal files
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A potential confused deputy vulnerability exists where a malicious Android app can trick Chrome into granting it READ and WRITE access to Chrome’s internal FileProvider. By sending a crafted intent with the NAVIGATE_NEW mode and attacker-supplied URIs in EXTRA_FILE_HANDLING_DATA, the attacker bypasses Digital Asset Links verification. Chrome blindly grants permissions to these URIs, exposing sensitive private files like cached PDFs and network export logs.
Affected files:
chrome/android/java/src/org/chromium/chrome/browser/customtabs/content/WebAppLaunchHandler.javachrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabIntentDataProvider.java
Estimated timestamp from git blame: 2025-10-03
Overview
A potential confused deputy vulnerability has been identified in Chrome for Android’s handling of Trusted Web Activity (TWA) intents. A malicious application can obtain persistent READ and WRITE URI permissions for Chrome’s private FileProvider paths by exploiting WebAppLaunchHandler’s processing of the EXTRA_FILE_HANDLING_DATA bundle.
Vulnerability Details
When Chrome handles a TWA intent with the EXTRA_LAUNCH_HANDLER_CLIENT_MODE set to 3 (NAVIGATE_NEW), it invokes WebAppLaunchHandler.launchNewIntent(). This specific client mode bypasses the standard Digital Asset Links (DAL) verification check that normally restricts sensitive operations to verified origins.
Inside launchNewIntent(), Chrome extracts a list of URIs from the EXTRA_FILE_HANDLING_DATA bundle. This data is supplied by the calling intent and is not validated by Chrome before use. The handler iterates through these URIs and calls mActivity.grantUriPermission() for each one, granting the calling package (the attacker’s app) both READ and WRITE access.
// WebAppLaunchHandler.java
if (fileData != null && !fileData.uris.isEmpty()) {
for (Uri uri : fileData.uris) {
mActivity.grantUriPermission(
packageName,
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
newIntent.putExtra(EXTRA_FILE_HANDLING_DATA, fileData.toBundle());
}
An attacker can provide URIs that point to Chrome’s own internal FileProvider (e.g., content://com.android.chrome.FileProvider/pdfs/...). Because Chrome is the owner of this FileProvider and its AndroidManifest.xml configures it with android:grantUriPermissions="true", the Android framework successfully processes Chrome’s request to grant these permissions to the attacker’s package.
Impact
By obtaining READ and WRITE access to Chrome’s FileProvider, a malicious app can exfiltrate sensitive user data stored in Chrome’s private cache directories. Paths exported in file_paths.xml include:
cache/pdfs/: Cached PDF files.cache/net-export/: Network export logs.cache/Offline Pages/archives/: Offline page archives.
The granted permissions persist until the device is rebooted.
Potential Attack Steps
(Note: These are suggested steps based on code analysis, as this agent cannot run live proofs-of-concept).
- A malicious application installed on the device binds to Chrome’s
CustomTabsServiceand obtains aCustomTabsSessionTokenvianewSession(). This associates the session with the attacker’s package name. - The app sends an initial
ACTION_VIEWintent to Chrome with a valid HTTPS URL, the session token, andEXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY=true. This starts the TWA process. - The app then sends a follow-up intent to the same activity (using
FLAG_ACTIVITY_SINGLE_TOP) with the following extras:EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY = trueEXTRA_LAUNCH_HANDLER_CLIENT_MODE = 3(NAVIGATE_NEW)EXTRA_FILE_HANDLING_DATA = [Bundle containing target Chrome FileProvider URIs, e.g., content://com.android.chrome.FileProvider/pdfs/sensitive.pdf]
BaseCustomTabActivityreceives this new intent and routes it toWebAppLaunchHandler.handleNewIntent().- Because
clientMode == NAVIGATE_NEW, DAL verification is bypassed, andlaunchNewIntent()is called. - Chrome iterates over the attacker-supplied URIs and calls
grantUriPermission()for the attacker’s package name. - The malicious app uses Android’s
ContentResolverto read from or write to the granted URIs, successfully exfiltrating or modifying private data.
Suggested Fix
In WebAppLaunchHandler.launchNewIntent(), validate the URIs provided in EXTRA_FILE_HANDLING_DATA before calling grantUriPermission(). Specifically, Chrome should verify that the URIs do not belong to its own FileProvider authority (e.g., com.android.chrome.FileProvider). Additionally, consider whether grantUriPermission is even necessary if the URIs already belong to the calling package.
Evaluated with Chrome root at commit: 0eb4855bda702feaaa8b899336664f97e3df88b8
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. Please feel free to reach out to me if you have concerns or feedback.