Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Network
DescriptionInappropriate implementation in Network
ComponentNetwork
Bug ClassLogic Error
Tracker497453475
Fix commiteeea0d096f3c (chromium/src) +160/-7
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
if
chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsConnection.java
modified

Files Changed

  • chrome/android/java/src/org/chromium/chrome/browser/customtabs/ClientManager.java
  • chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabIntentDataProvider.java
  • chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsConnection.java
  • chrome/android/junit/src/org/chromium/chrome/browser/customtabs/ClientManagerTest.java
From eeea0d096f3cda2df74325511cd2f779d1d01f65 Mon Sep 17 00:00:00 2001
From: Yuyang Huang <[email protected]>
Date: Thu, 14 May 2026 05:23:57 -0700
Subject: [PATCH] Protect CCT multi-networking with network permissions

Restrict access to the CCT multi-networking API by requiring either the
MAINLINE_NETWORK_STACK or NETWORK_SETTINGS permission. The permission
check is enforced against the client's UID and PID resolved from the
CCT session.

Bug: 497453475
Change-Id: Ie83ebf53e269f45946dcca172926f0b66db751aa
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7828908
Reviewed-by: Stefano Duo <[email protected]>
Commit-Queue: Jinsuk Kim <[email protected]>
Auto-Submit: Yuyang Huang <[email protected]>
Reviewed-by: Jinsuk Kim <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1630583}
---

diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/ClientManager.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/ClientManager.java
index 4769b569..24443f1 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/ClientManager.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/ClientManager.java
@@ -205,6 +205,7 @@
     /** Per-session values. */
     private static class SessionParams {
         public final int uid;
+        public final int pid;
         private @Nullable BrowserCallbackWrapper mCallback;
         private @Nullable EngagementSignalsCallback mEngagementSignalsCallback;
         public final DisconnectCallback disconnectCallback;
@@ -234,12 +235,14 @@
         public SessionParams(
                 Context context,
                 int uid,
+                int pid,
                 BrowserCallbackWrapper callback,
                 DisconnectCallback disconnectCallback,
                 @Nullable PostMessageHandler postMessageHandler,
                 @Nullable PostMessageServiceConnection serviceConnection,
                 @Nullable EngagementSignalsHandler engagementSignalsHandler) {
             this.uid = uid;
+            this.pid = pid;
             mPackageName = getPackageName(context, uid);
             mCallback = callback;
             this.disconnectCallback = disconnectCallback;
@@ -374,6 +377,7 @@
     public synchronized boolean newSession(
             SessionHolder<?> session,
             int uid,
+            int pid,
             DisconnectCallback onDisconnect,
             @Nullable PostMessageHandler postMessageHandler,
             @Nullable PostMessageServiceConnection serviceConnection,
@@ -403,6 +407,7 @@
                     new SessionParams(
                             ContextUtils.getApplicationContext(),
                             uid,
+                            pid,
                             callbackWrapper,
                             onDisconnect,
                             postMessageHandler,
@@ -712,6 +717,20 @@
     }
 
     /**
+     * @return The UID associated with the client owning the given session.
+     */
+    public int getClientUidForSession(@Nullable SessionHolder<?> session) {
+        return callOnSession(session, -1, params -> params.uid);
+    }
+
+    /**
+     * @return The PID associated with the client owning the given session.
+     */
+    public int getClientPidForSession(@Nullable SessionHolder<?> session) {
+        return callOnSession(session, -1, params -> params.pid);
+    }
+
+    /**
      * Overrides the package name for the given session to be the given package name. To be used for
      * testing only.
      */
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 248e86d..9337bf8 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
@@ -25,7 +25,6 @@
 import static androidx.browser.customtabs.CustomTabsIntent.EXTRA_CLOSE_BUTTON_POSITION;
 import static androidx.browser.customtabs.CustomTabsIntent.EXTRA_INITIAL_ACTIVITY_HEIGHT_PX;
 import static androidx.browser.customtabs.CustomTabsIntent.EXTRA_INITIAL_ACTIVITY_WIDTH_PX;
-import static androidx.browser.customtabs.CustomTabsIntent.EXTRA_NETWORK;
 import static androidx.browser.customtabs.CustomTabsIntent.EXTRA_TITLE_VISIBILITY_STATE;
 import static androidx.browser.customtabs.CustomTabsIntent.EXTRA_TOOLBAR_CORNER_RADIUS_DP;
 import static androidx.browser.trusted.LaunchHandlerClientMode.FOCUS_EXISTING;
@@ -582,7 +581,7 @@
 
         mKeepAliveServiceIntent = IntentUtils.safeGetParcelableExtra(intent, EXTRA_KEEP_ALIVE);
 
-        mNetwork = IntentUtils.safeGetParcelableExtra(intent, EXTRA_NETWORK);
+        mNetwork = CustomTabsConnection.getInstance().extractTargetNetwork(intent, mSession);
 
         mIsOpenedByChrome = IntentHandler.wasIntentSenderChrome(intent);
 
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 67ce3b73..472dfc1 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
@@ -9,8 +9,11 @@
 
 import android.app.PendingIntent;
 import android.content.ComponentCallbacks2;
+import android.content.Context;
 import android.content.Intent;
+import android.content.pm.PackageManager;
 import android.graphics.Bitmap;
+import android.net.Network;
 import android.net.Uri;
 import android.os.Binder;
 import android.os.Bundle;
@@ -105,6 +108,7 @@
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Consumer;
@@ -409,6 +413,7 @@
         return mClientManager.newSession(
                 session,
                 Binder.getCallingUid(),
+                Binder.getCallingPid(),
                 onDisconnect,
                 postMessageHandler,
                 serviceConnection,
@@ -1375,6 +1380,47 @@
     }
 
     /**
+     * Extracts the target network from the intent if the caller has the required permissions.
+     * Package-private to be used by {@link CustomTabIntentDataProvider}.
+     */
+    @Nullable Network extractTargetNetwork(Intent intent, @Nullable SessionHolder<?> session) {
+        Network network =
+                IntentUtils.safeGetParcelableExtra(intent, CustomTabsIntent.EXTRA_NETWORK);
+        if (network == null) return null;
+
+        int uid = mClientManager.getClientUidForSession(session);
+        int pid = mClientManager.getClientPidForSession(session);
+        String callerPackageName = mClientManager.getClientPackageNameForSession(session);
+        String callerIdentity =
+                callerPackageName != null
+                        ? String.format(
+                                Locale.US, "%s (UID %d, PID %d)", callerPackageName, uid, pid)
+                        : String.format(Locale.US, "UID %d, PID %d", uid, pid);
+
+        Context context = ContextUtils.getApplicationContext();
+        boolean hasPermission =
+                context.checkPermission("android.permission.MAINLINE_NETWORK_STACK", pid, uid)
+                                == PackageManager.PERMISSION_GRANTED
+                        || context.checkPermission("android.permission.NETWORK_SETTINGS", pid, uid)
+                                == PackageManager.PERMISSION_GRANTED;
+
+        if (!hasPermission) {
+            Log.w(
+                    TAG,
+                    "Stripping EXTRA_NETWORK: caller %s does not have the required network"
+                            + " permission. The Custom Tab will use the default network.",
+                    callerIdentity);
+            return null;
+        }
+
+        Log.i(
+                TAG,
+                "Allowed to use EXTRA_NETWORK: caller %s has required network permission.",
+                callerIdentity);
+        return network;
+    }
+
+    /**
      * @return Whether the given package name is that of a first-party application.
      */
     public boolean isFirstParty(@Nullable String packageName) {
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/ClientManagerTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/ClientManagerTest.java
index 6871b66..72e95e3a 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/ClientManagerTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/ClientManagerTest.java
@@ -71,6 +71,7 @@
     private final SessionHolder<?> mSession =
             new SessionHolder<>(CustomTabsSessionToken.createMockSessionTokenForTesting());
     private final int mUid = Process.myUid();
+    private final int mPid = Process.myPid();
 
     private EngagementSignalsHandler mEngagementSignalsHandler;
     private PostMessageServiceConnection mPostMessageServiceConnection;
@@ -167,6 +168,7 @@
         mClientManager.newSession(
                 mSession,
Loading diff…

Original Bug Report

reported by [email protected]

Potential Site Isolation Bypass via CORS-Disabled URLLoaderFactory in Multi-Network CCTs

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: In Android multi-network Chrome Custom Tabs, a logic flaw in ChromeContentBrowserClient::MaybeProxyNetworkBoundRequest replaces scheme-specific subresource factories (like data:) with a highly privileged, CORS-disabled CorsURLLoaderFactory. A compromised renderer can misuse this factory’s Mojo pipe to issue arbitrary network requests that bypass CORS and ORB, leading to a Site Isolation bypass for the bound network.

Affected files:

  • chrome/browser/chrome_content_browser_client.cc
  • content/browser/renderer_host/render_frame_host_impl.cc

Estimated timestamp from git blame: 2026-02-18

Summary

There is a potential Site Isolation bypass in the implementation of multi-network support for Chrome Custom Tabs (CCT) on Android. When a CCT is bound to a specific network (e.g., via the EXTRA_NETWORK intent), the browser attempts to proxy all network requests through a network-bound NetworkContext. However, for scheme-specific subresource factories (such as those for data: and filesystem: schemes), the proxying logic incorrectly replaces the intended restricted factory with a highly privileged CorsURLLoaderFactory that has CORS disabled, is marked as trusted, and identifies as the browser process. A compromised renderer can use the Mojo pipe originally intended for these restricted schemes to issue arbitrary network requests that bypass CORS, ORB, and other renderer-side security checks.

Technical Details

  1. Factory Creation: In RenderFrameHostImpl::CommitNavigation, the browser creates a bundle of subresource loader factories for the renderer. For non-network schemes (like data:), it wraps the factory using url_loader_factory::CreateAndConnectToPendingReceiver.
  2. Disallowing Overrides: This wrapping function uses TerminalParams::ForNonNetwork, which explicitly sets FactoryOverrideOption::kDisallow. Consequently, the factory_override pointer passed to the embedder’s ChromeContentBrowserClient::WillCreateURLLoaderFactory is nullptr.
  3. The Flaw in MaybeProxyNetworkBoundRequest: For multi-network CCTs, WillCreateURLLoaderFactory calls MaybeProxyNetworkBoundRequest at the end of its interceptor chain. Because factory_override is null, the code enters a branch intended for single-layer factories:
    // chrome/browser/chrome_content_browser_client.cc
    if (!factory_override) {
      // Hijack the receiver end returned by network::URLLoaderFactoryBuilder.
      // This will be then redirected to a network-bound URLLoaderFactory.
      std::tie(proxied_receiver, bypassed_remote) = factory_builder.Append();
    } 
    
  4. Dropping the Original Factory: The code then creates a new URLLoaderFactory on the network-bound NetworkContext using proxied_receiver and the following highly privileged parameters:
    params->process_id = network::OriginatingProcessId::browser();
    params->is_trusted = true;
    params->disable_web_security = true;
    
    Critically, the local variable bypassed_remote (which points to the original, restricted scheme-specific factory) is never used and is dropped when the function returns. The Mojo pipe sent to the renderer as a scheme-specific factory is now connected directly to this browser-privileged CorsURLLoaderFactory in the Network Service.

Suggested Exploitation Steps

Note: These steps have not been verified with a working Proof of Concept as our tooling agent cannot execute code.

  1. Precondition: An attacker compromises a renderer process (e.g., via a separate V8 vulnerability) that is hosting an Android CCT launched with a specific target network (multi-network CCT).
  2. Pipe Extraction: The compromised renderer receives the PendingURLLoaderFactoryBundle during navigation commit and extracts the Mojo pipe labeled for a non-network scheme (e.g., the data: scheme factory).
  3. Crafting the Request: Instead of a data: request, the attacker crafts a network::ResourceRequest targeting an arbitrary HTTP/HTTPS URL (e.g., https://victim.example/). They set the request_mode to kCors to request cross-origin data.
  4. Bypassing Security Checks: The attacker sends this request over the extracted Mojo pipe.
    • The CorsURLLoaderFactory in the Network Service skips CORS enforcement because disable_web_security was set to true during its creation.
    • It skips request validation and request_initiator_origin_lock checks because process_id_.is_browser() is true.
    • ORB (Opaque Response Blocking) does not block the response because the attacker set the request mode to kCors, classifying the response as non-opaque.
  5. Data Exfiltration: The Network Service fetches the cross-origin HTTP/HTTPS resource and returns the complete, unredacted response to the compromised renderer, successfully bypassing Site Isolation for the bound network.

Proposed Fix

In ChromeContentBrowserClient::MaybeProxyNetworkBoundRequest, the logic for hijacking the builder chain when factory_override is null is flawed because it drops the terminal factory (bypassed_remote). The function should either:

  1. Be updated to correctly handle single-layer factories by ensuring the new network-bound factory connects to the original terminal factory (if one exists), rather than dropping it.
  2. Alternatively, the network-binding proxy logic should explicitly avoid intercepting factories intended for non-network schemes (like data: or filesystem:), as they do not require network binding and should remain restricted.

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