CVE-2026-79286
Overview
Files Changed
chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsConnection.javachrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsConnectionServiceImpl.javachrome/android/junit/src/org/chromium/chrome/browser/customtabs/CustomTabsConnectionUnitTest.java
Patch
From a34728fb28d14bc2c8fb1bf3f23d34b8e0763261 Mon Sep 17 00:00:00 2001
From: Mohamed Adel <adelm@google.com>
Date: Wed, 01 Jul 2026 16:29:53 -0700
Subject: [PATCH] [Android] Require a registered session for receiveFile
Unlike the other CustomTabsService entry points, receiveFile did not
check that the caller had completed first run or that the supplied
session token had been registered via newSession() before opening the
supplied URI and decoding it as a TWA splash image in
CustomTabsClientFileProcessor.
Bring it in line with warmup/mayLaunchUrl/updateVisuals etc:
* CustomTabsConnectionServiceImpl.receiveFile now returns false
before first run completes.
* CustomTabsConnection.receiveFile now requires the session to
be known to ClientManager before any file is opened.
Add Robolectric coverage for receiveFile that exercises both the
rejected (unregistered session) and accepted (registered session) paths.
TAG=agy
CONV=aba571cb-6002-44d9-a887-67df13c38ac3
Bug: b:497940451
Change-Id: Ifdbf0a8b86c3a9bdaa3d444956cd7e56fa0384f4
Fixed: 497940451
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8032890
Commit-Queue: Moe Adel <adelm@google.com>
Auto-Submit: Moe Adel <adelm@google.com>
Reviewed-by: Joel Tan-Aristy <jtanaristy@google.com>
Cr-Commit-Position: refs/heads/main@{#1655817}
---
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsConnection.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsConnection.java
index 5688cf5b..bf36da7d 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsConnection.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsConnection.java
@@ -2155,8 +2155,10 @@
logCall("receiveFile()", false);
return false;
}
+ SessionHolder<?> session = new SessionHolder<>(sessionToken);
+ if (!mClientManager.isSessionValid(session)) return false;
return CustomTabsClientFileProcessor.getInstance()
- .processFile(new SessionHolder<>(sessionToken), uri, purpose, extras);
+ .processFile(session, uri, purpose, extras);
}
public void setCustomTabIsInForeground(
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsConnectionServiceImpl.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsConnectionServiceImpl.java
index ffceaff..a1ab4a9d 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsConnectionServiceImpl.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsConnectionServiceImpl.java
@@ -131,6 +131,7 @@
@Override
protected boolean receiveFile(
CustomTabsSessionToken sessionToken, Uri uri, int purpose, @Nullable Bundle extras) {
+ if (!isFirstRunDone()) return false;
return mConnection.receiveFile(sessionToken, uri, purpose, extras);
}
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/CustomTabsConnectionUnitTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/CustomTabsConnectionUnitTest.java
index b63618d2..eb39946e 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/CustomTabsConnectionUnitTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/CustomTabsConnectionUnitTest.java
@@ -374,6 +374,7 @@
@Config(sdk = {BaseRobolectricTestRunner.MIN_SDK, 35})
@Test
public void receiveFile_CallerWithUriPermission() throws IOException {
+ initSession();
Uri uri = registerSplashImageProvider();
setCallerUriPermission(PackageManager.PERMISSION_GRANTED);
@@ -387,5 +388,20 @@
assertNotNull(SplashImageHolder.getInstance().takeImage(mSessionHolder));
}
+ @Config(sdk = {BaseRobolectricTestRunner.MIN_SDK, 35})
+ @Test
+ public void receiveFile_SessionNotRegistered() throws IOException {
+ Uri uri = registerSplashImageProvider();
+ setCallerUriPermission(PackageManager.PERMISSION_GRANTED);
+
+ assertFalse(
+ mConnection.receiveFile(
+ mSession,
+ uri,
+ CustomTabsService.FILE_PURPOSE_TRUSTED_WEB_ACTIVITY_SPLASH_IMAGE,
+ Bundle.EMPTY));
+ assertFalse(TestSplashImageContentProvider.sOpened);
+ assertNull(SplashImageHolder.getInstance().takeImage(mSessionHolder));
+ }
// TODO(https://crrev.com/c/4118209) Add more tests for Feature enabling/disabling.
}
Original Bug Report
Potential Sandbox Bypass / Rule of 2 Violation via CustomTabsService.receiveFile
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: The exported CustomTabsConnectionService allows any installed Android app to trigger image decoding of an attacker-controlled URI directly in the browser process. This bypasses the sandboxed DecoderService and violates the Rule of 2 by parsing untrusted input using memory-unsafe native code (Skia) in a highly privileged process.
Affected files:
chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsConnection.javachrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsClientFileProcessor.javachrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsConnectionServiceImpl.javachrome/android/java/src/org/chromium/chrome/browser/base/SplitCompatCustomTabsService.javabase/android/java/src/org/chromium/base/FileUtils.java
Estimated timestamp from git blame: 2025-09-08
Description
Chrome for Android exposes the CustomTabsConnectionService, which is exported in the manifest without requiring any permissions to bind. This service exposes the receiveFile AIDL method. Any installed application can connect to this service and invoke receiveFile with an arbitrary URI.
When receiveFile is called with the purpose FILE_PURPOSE_TRUSTED_WEB_ACTIVITY_SPLASH_IMAGE, the following call chain occurs entirely within the highly privileged browser process:
SplitCompatCustomTabsService.receiveFiledelegates toCustomTabsConnectionServiceImpl.receiveFile.CustomTabsConnection.receiveFileroutes the request toCustomTabsClientFileProcessor.processFilewithout validating the caller’s UID or checking if the caller is a verified Trusted Web Activity (TWA).CustomTabsClientFileProcessor.receiveTwaSplashImageis invoked, which callsFileUtils.queryBitmapFromContentProvider(context, uri).FileUtils.queryBitmapFromContentProvideropens aParcelFileDescriptorfrom the attacker-supplied URI and directly calls the Android framework APIBitmapFactory.decodeFileDescriptor.
Security Impact
BitmapFactory is a Java facade that relies on the native Android Skia library (written in C++) to parse and decode image formats. Because CustomTabsConnectionService runs in the main browser process, this architecture forces the browser process to parse untrusted, attacker-controlled data using a memory-unsafe language.
This violates Chromium’s “Rule of 2” and bypasses Chrome’s existing sandboxing mechanisms for image decoding (such as DecoderService, which uses seccomp-bpf to safely process untrusted images).
If an attacker provides a malformed image designed to exploit a memory corruption vulnerability (N-day or 0-day) in the Android Skia library, they could achieve arbitrary code execution within the browser process. This would lead to a full sandbox escape, granting the malicious app access to cookies, saved passwords, and other sensitive user data.
Potential Reproduction Steps
Note: These are suggested steps; we do not currently have a weaponized proof-of-concept.
- Create a malicious Android application with a
ContentProviderthat serves a malformed image (e.g., a crafted WebP or PNG known to trigger a Skia vulnerability). - Have the malicious app bind to Chrome’s
CustomTabsConnectionServiceusing the intent actionandroid.support.customtabs.action.CustomTabsService. - Invoke the
receiveFilemethod on theICustomTabsServicebinder, providing a dummy session token, the URI to the maliciousContentProvider, and settingpurposeto1(FILE_PURPOSE_TRUSTED_WEB_ACTIVITY_SPLASH_IMAGE). - Chrome’s browser process will open the URI and pass the file descriptor to
BitmapFactory.decodeFileDescriptor, triggering the vulnerability in the browser process heap.
Suggested Fix
- Sandbox the Decoding: Do not use
BitmapFactory.decodeFileDescriptorin the browser process. Instead, route the image decoding through a sandboxed utility process, such as Chrome’sDecoderService, which is specifically designed to safely parse untrusted image data. - Caller Validation: Implement proper validation in
CustomTabsConnection.receiveFileto ensure that the calling UID (Binder.getCallingUid()) matches the UID associated with the session, and verify that the caller is a legitimate TWA before processing the file.
Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0
Results from 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.