Medium watch CVSS 7.4 chrome Authorization Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
7.4
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMissing authorization in CustomTabs
DescriptionMissing authorization in CustomTabs
ComponentCustomTabs
Sub-componentAndroid
Bug ClassAuthorization Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker497940451
Fix commita34728fb28d1 (chromium/src) +20/-1
CWECWE-862
CVSS vectorCVSS:3.1/AV:L/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-03-30 (per vendor advisory)
Fix landed2026-07-01 (public git)
Disclosed2026-08-25
Patch gap55 days — fix public in source before the advisory

Background

`CustomTabsService.receiveFile`
An Android CustomTabs entry point that lets a client app hand Chrome a Uri and a purpose, e.g. a Trusted Web Activity (TWA) splash image, for Chrome to open and decode.
Session token
A CustomTabsSessionToken that a client normally registers via newSession(); a registered token is what ClientManager uses to tie subsequent calls to an authorized client.
`ClientManager.isSessionValid`
The check that confirms a supplied session was actually registered with the browser before privileged work is done on the caller’s behalf.
First run
Chrome’s one-time initialization/consent gate; most service entry points refuse to act until isFirstRunDone() returns true.

Root Cause Analysis

CustomTabsConnectionServiceImpl.receiveFile forwarded straight to CustomTabsConnection.receiveFile, which built a SessionHolder<> from the caller-supplied sessionToken and passed it directly to CustomTabsClientFileProcessor.getInstance().processFile(...) without ever verifying the session was registered or that first run had completed. Unlike sibling entry points (warmup, mayLaunchUrl, updateVisuals), it therefore opened and decoded the supplied uri as a TWA splash image for an arbitrary, unregistered caller. The violated invariant is that a caller must have completed first run and hold a session known to ClientManager before Chrome will open a caller-supplied Uri on its behalf.

The fix adds if (!isFirstRunDone()) return false; in the service impl and, in CustomTabsConnection.receiveFile, constructs the session and returns false when !mClientManager.isSessionValid(session) before any file is opened. Both guards run and early-return before processFile is reached, so an unregistered session can no longer drive file processing.

Key insight
receiveFile was the one CustomTabs entry point missing the standard first-run and registered-session authorization checks, so it processed a caller-supplied Uri for any client; the fix restores parity by gating on isFirstRunDone() and mClientManager.isSessionValid(session) before processFile.

Attack Path

  1. Reach the entry point A malicious/unprivileged app binds to Chrome’s CustomTabsService and calls receiveFile with a CustomTabsSessionToken it never registered via newSession().
  2. Bypass authorization Because the old code neither checked isFirstRunDone() nor mClientManager.isSessionValid(session), control passed directly into CustomTabsClientFileProcessor.processFile.
  3. Trigger file open/decode With purpose set to FILE_PURPOSE_TRUSTED_WEB_ACTIVITY_SPLASH_IMAGE, Chrome opens the supplied uri and decodes it as a TWA splash image on behalf of the unauthorized caller.
  4. Consequence The unauthorized processing of a caller-controlled Uri constitutes the missing-authorization defect the advisory describes; any further impact from decoding attacker-chosen content is (inferred).

Impact Assessment

An unprivileged local Android application gains the ability to make Chrome open and decode a caller-supplied Uri as a TWA splash image without registering a session or completing first run, i.e. missing authorization in CustomTabs as the advisory states. The precondition is local code execution on the device able to bind to the CustomTabs service; the advisory does not report in-the-wild exploitation. Any escalation beyond the unauthorized file open/decode shown by the patch is (inferred).

Files Changed

  • chrome/android/junit/src/org/chromium/chrome/browser/customtabs/CustomTabsConnectionUnitTest.java
  • chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsConnection.java
  • chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsConnectionServiceImpl.java

Audit Directions

  • Entry-point parity
    Enumerate every CustomTabsService/CustomTabsConnection entry point and confirm each applies the same first-run and isSessionValid gates; a method that skips one is a likely authorization gap.
  • Caller-supplied Uri sinks
    Audit any IPC/service path that opens or decodes a caller-provided Uri (splash images, icons, other resources) and verify the session is validated before the resource is touched.
  • Guard-before-sink ordering
    For newly added authorization checks, confirm the early return false executes before the privileged operation (here processFile) rather than after object construction that itself has side effects.
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.
 }
Loading diff…

Original Bug Report

reported by vm...@google.com

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.java
  • chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsClientFileProcessor.java
  • chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsConnectionServiceImpl.java
  • chrome/android/java/src/org/chromium/chrome/browser/base/SplitCompatCustomTabsService.java
  • base/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:

  1. SplitCompatCustomTabsService.receiveFile delegates to CustomTabsConnectionServiceImpl.receiveFile.
  2. CustomTabsConnection.receiveFile routes the request to CustomTabsClientFileProcessor.processFile without validating the caller’s UID or checking if the caller is a verified Trusted Web Activity (TWA).
  3. CustomTabsClientFileProcessor.receiveTwaSplashImage is invoked, which calls FileUtils.queryBitmapFromContentProvider(context, uri).
  4. FileUtils.queryBitmapFromContentProvider opens a ParcelFileDescriptor from the attacker-supplied URI and directly calls the Android framework API BitmapFactory.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.

  1. Create a malicious Android application with a ContentProvider that serves a malformed image (e.g., a crafted WebP or PNG known to trigger a Skia vulnerability).
  2. Have the malicious app bind to Chrome’s CustomTabsConnectionService using the intent action android.support.customtabs.action.CustomTabsService.
  3. Invoke the receiveFile method on the ICustomTabsService binder, providing a dummy session token, the URI to the malicious ContentProvider, and setting purpose to 1 (FILE_PURPOSE_TRUSTED_WEB_ACTIVITY_SPLASH_IMAGE).
  4. 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

  1. Sandbox the Decoding: Do not use BitmapFactory.decodeFileDescriptor in the browser process. Instead, route the image decoding through a sandboxed utility process, such as Chrome’s DecoderService, which is specifically designed to safely parse untrusted image data.
  2. Caller Validation: Implement proper validation in CustomTabsConnection.receiveFile to 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.

View on issue tracker