Chrome · CustomTabs
CVE-2026-79086
Logic Error in CustomTabs
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Files Changed
chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsConnection.javachrome/android/junit/src/org/chromium/chrome/browser/customtabs/CustomTabsConnectionUnitTest.java
Patch
From f7e33e945ab5509c38b18b0d6feb18d59600123f Mon Sep 17 00:00:00 2001 From: Mohamed Adel <[email protected]> Date: Wed, 01 Jul 2026 14:47:26 -0700 Subject: [PATCH] [CCT] Check caller URI permission in receiveFile() CustomTabsConnection.receiveFile() opens a client-supplied URI on behalf of the bound client app to load a TWA splash image. Ensure the bound client itself holds read access to that URI before opening it under the browser's identity. Adds Robolectric coverage for the granted and denied cases. Bug: 511804361 Fixed: 511804361 Change-Id: Ic1fcaa0e3d4a40fa0897d479dff2d6b102b711f8 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8031565 Reviewed-by: Joel Tan-Aristy <[email protected]> Auto-Submit: Moe Adel <[email protected]> Commit-Queue: Joel Tan-Aristy <[email protected]> Cr-Commit-Position: refs/heads/main@{#1655765} --- 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 e82fd80..5688cf5b 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 @@ -2145,6 +2145,16 @@ public boolean receiveFile( CustomTabsSessionToken sessionToken, Uri uri, int purpose, @Nullable Bundle extras) { + if (ContextUtils.getApplicationContext() + .checkUriPermission( + uri, + Binder.getCallingPid(), + Binder.getCallingUid(), + Intent.FLAG_GRANT_READ_URI_PERMISSION) + != PackageManager.PERMISSION_GRANTED) { + logCall("receiveFile()", false); + return false; + } return CustomTabsClientFileProcessor.getInstance() .processFile(new SessionHolder<>(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 94d4582..b63618d2 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 @@ -8,6 +8,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -28,13 +29,23 @@ import static org.chromium.chrome.browser.customtabs.CustomTabsConnection.ON_ACTIVITY_LAYOUT_TOP_EXTRA; import android.app.PendingIntent; +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.Context; +import android.content.ContextWrapper; import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ProviderInfo; +import android.database.Cursor; import android.net.Network; +import android.net.Uri; import android.os.Bundle; +import android.os.ParcelFileDescriptor; import android.os.Process; import androidx.browser.customtabs.CustomTabsCallback; import androidx.browser.customtabs.CustomTabsIntent; +import androidx.browser.customtabs.CustomTabsService; import androidx.browser.customtabs.CustomTabsSessionToken; import androidx.browser.customtabs.EngagementSignalsCallback; import androidx.browser.customtabs.PostMessageServiceConnection; @@ -43,14 +54,17 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import org.robolectric.Robolectric; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowProcess; +import org.chromium.base.ContextUtils; import org.chromium.base.test.BaseRobolectricTestRunner; import org.chromium.base.test.util.Batch; import org.chromium.base.test.util.Features.DisableFeatures; @@ -59,11 +73,17 @@ import org.chromium.chrome.browser.browserservices.SessionDataHolder; import org.chromium.chrome.browser.browserservices.SessionHandler; import org.chromium.chrome.browser.browserservices.intents.SessionHolder; +import org.chromium.chrome.browser.browserservices.ui.splashscreen.trustedwebactivity.SplashImageHolder; import org.chromium.chrome.browser.customtabs.content.EngagementSignalsHandler; import org.chromium.chrome.browser.flags.ChromeFeatureList; import org.chromium.chrome.browser.privacy.settings.PrivacyPreferencesManagerImpl; import org.chromium.chrome.browser.tab.Tab; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; + /** Tests for some parts of {@link CustomTabsConnection}. */ @RunWith(BaseRobolectricTestRunner.class) @Batch(Batch.UNIT_TESTS) @@ -71,6 +91,7 @@ public class CustomTabsConnectionUnitTest { @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Rule public final TemporaryFolder mTemporaryFolder = new TemporaryFolder(); @Mock private SessionHandler mSessionHandler; @Mock private CustomTabsCallback mCallback; @@ -263,5 +284,108 @@ assertNull(mConnection.extractTargetNetwork(intent, mSessionHolder)); } + /** Content provider that records whether it was opened. */ + public static class TestSplashImageContentProvider extends ContentProvider { + static boolean sOpened; + static File sImageFile; + + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { + sOpened = true; + return ParcelFileDescriptor.open(sImageFile, ParcelFileDescriptor.MODE_READ_ONLY); + } + + @Override + public boolean onCreate() { + return false; + } + + @Override + public Cursor query( + Uri uri, + String[] projection, + String selection, + String[] selectionArgs, + String sortOrder) { + return null; + } + + @Override + public String getType(Uri uri) { + return null; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + return null; + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + return 0; + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + return 0; + } + } + + private Uri registerSplashImageProvider() throws IOException { + TestSplashImageContentProvider.sOpened = false; + TestSplashImageContentProvider.sImageFile = mTemporaryFolder.newFile("splash.png"); + try (FileOutputStream stream = + new FileOutputStream(TestSplashImageContentProvider.sImageFile)) { + stream.write("non-empty".getBytes()); + } + ProviderInfo info = new ProviderInfo(); + info.authority = "org.chromium.test.splash"; + Robolectric.buildContentProvider(TestSplashImageContentProvider.class).create(info); + return Uri.parse("content://org.chromium.test.splash/splash.png"); + } + + private void setCallerUriPermission(int result) { + Context context = + new ContextWrapper(ContextUtils.getApplicationContext()) { + @Override + public int checkUriPermission(Uri uri, int pid, int uid, int modeFlags) { + return result; + } + }; + ContextUtils.initApplicationContextForTests(context); + } + + @Config(sdk = {BaseRobolectricTestRunner.MIN_SDK, 35}) + @Test
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