CVE-2026-13868
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsConnection.java |
modified |
Files Changed
chrome/android/java/src/org/chromium/chrome/browser/customtabs/ClientManager.javachrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabIntentDataProvider.javachrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsConnection.javachrome/android/junit/src/org/chromium/chrome/browser/customtabs/ClientManagerTest.java
Patch
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,
Original Bug Report
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.cccontent/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
- Factory Creation: In
RenderFrameHostImpl::CommitNavigation, the browser creates a bundle of subresource loader factories for the renderer. For non-network schemes (likedata:), it wraps the factory usingurl_loader_factory::CreateAndConnectToPendingReceiver. - Disallowing Overrides: This wrapping function uses
TerminalParams::ForNonNetwork, which explicitly setsFactoryOverrideOption::kDisallow. Consequently, thefactory_overridepointer passed to the embedder’sChromeContentBrowserClient::WillCreateURLLoaderFactoryisnullptr. - The Flaw in
MaybeProxyNetworkBoundRequest: For multi-network CCTs,WillCreateURLLoaderFactorycallsMaybeProxyNetworkBoundRequestat the end of its interceptor chain. Becausefactory_overrideis 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(); } - Dropping the Original Factory: The code then creates a new
URLLoaderFactoryon the network-boundNetworkContextusingproxied_receiverand the following highly privileged parameters:Critically, the local variableparams->process_id = network::OriginatingProcessId::browser(); params->is_trusted = true; params->disable_web_security = true;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-privilegedCorsURLLoaderFactoryin 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.
- 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).
- Pipe Extraction: The compromised renderer receives the
PendingURLLoaderFactoryBundleduring navigation commit and extracts the Mojo pipe labeled for a non-network scheme (e.g., thedata:scheme factory). - Crafting the Request: Instead of a
data:request, the attacker crafts anetwork::ResourceRequesttargeting an arbitrary HTTP/HTTPS URL (e.g.,https://victim.example/). They set therequest_modetokCorsto request cross-origin data. - Bypassing Security Checks: The attacker sends this request over the extracted Mojo pipe.
- The
CorsURLLoaderFactoryin the Network Service skips CORS enforcement becausedisable_web_securitywas set totrueduring its creation. - It skips request validation and
request_initiator_origin_lockchecks becauseprocess_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.
- The
- 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:
- 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.
- Alternatively, the network-binding proxy logic should explicitly avoid intercepting factories intended for non-network schemes (like
data:orfilesystem:), 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.