CVE-2026-95358
Overview
Background
- `IntentHandler`
- The Chrome-for-Android class that inspects incoming
Intentobjects and decides what URL to load and with what context. - `extractUrlFromIntent`
- A helper in
IntentHandlerthat walks a chain of extractors to recover a target URL from an incomingIntent. - `CUSTOM_TAB_SCHEME`
- A
"customtab"URI scheme constant, once used by a document-mode prototype to smuggle a target URL as theIntentdata URI’s query string. - `getUrlForCustomTab`
- The extractor that returned
data.getQuery()when anIntent’s data URI carried thecustomtabscheme, bypassing normal URL parsing.
Root Cause Analysis
The extractUrlFromIntent chain in IntentHandler.java included a call to getUrlForCustomTab, which trusted any Intent whose data URI used the customtab scheme and returned that URI’s raw query component as the URL to load. This code was leftover from a document-mode prototype, was unused by Chrome, and was not advertised in the AndroidManifest, yet it remained reachable because any app could craft an explicit Intent at Chrome’s launcher activity with a customtab: data URI. Because the query string was returned directly rather than being parsed and validated like an ordinary data URI, the path constituted an incorrect-authorization / logic error where an unintended, less-scrutinized channel could influence which URL Chrome navigated to.
The fix removes the getUrlForCustomTab call from extractUrlFromIntent, deletes the method itself, and drops the CUSTOM_TAB_SCHEME constant from UrlConstants.java, eliminating the scheme as a recognized input entirely. With the extractor gone, customtab: intents fall through to the remaining well-vetted extractors, closing the unauthorized path.
customtab scheme and returned its unparsed query as a URL; the fix removes that extractor and its scheme constant so the input is no longer recognized at all.Attack Path
- Craft a custom-tab intent
A malicious local app builds an explicit
Intenttargeting Chrome’s launcher activity with a data URI using thecustomtabscheme and an attacker-chosen query string. - Reach the extractor
Chrome’s
IntentHandlercallsextractUrlFromIntent, which invokesgetUrlForCustomTaband returnsdata.getQuery()without the validation applied to ordinary URLs. - Influence navigation
The returned query is treated as the target URL, letting the caller steer Chrome toward an attacker-controlled destination through an unintended channel.
(inferred) - Incorrect authorization outcome
The load proceeds via a path that was never meant to be exposed, matching the advisory’s “Incorrect authorization in Mobile” classification.
(inferred)
Impact Assessment
customtab scheme, an incorrect-authorization logic error in the mobile browser process. The precondition is the ability to deliver a crafted explicit Intent to Chrome on Android; the advisory states no exploited-in-the-wild status, and any escalation beyond influencing URL extraction is not demonstrated by the patch (inferred).Files Changed
chrome/android/java/src/org/chromium/chrome/browser/IntentHandler.javacomponents/embedder_support/android/java/src/org/chromium/components/embedder_support/util/UrlConstants.java
Audit Directions
- Dead prototype code pathsAudit
IntentHandlerand other entry points for extractors and schemes kept alive from abandoned prototypes that remain reachable via explicit intents despite not being advertised in theAndroidManifest. - Unparsed URI componentsFlag any code that returns
Uri.getQuery(),getFragment(), or similar raw components as a navigable URL without the validation applied to normal URLs. - Scheme-gated trustReview all
UrlConstantsscheme comparisons that grant special handling based solely ondata.getScheme(), confirming each has a legitimate, manifest-backed caller and proper authorization checks.
Patch
From eb82685080622cf8f9cca0197f30e6606b921185 Mon Sep 17 00:00:00 2001
From: Michael Thiessen <mthiesse@chromium.org>
Date: Mon, 31 Aug 2026 09:15:14 -0700
Subject: [PATCH] Reland "Remove CustomTabs scheme from Intent Handling"
(This change was incorrectly reverted and not responsible for
HistoryTabHelperTest#testAppHistory failing)
This code was added for a document mode prototype:
https://codereview.chromium.org/1838483003
It's unused in Chrome, and our AndroidManifest does not advertise
support for it. I really doubt we need a kill switch for this as there's
no legitimate reason for this to be used and would require explicit
intents aimed at our launcher activity.
Bug: 553141660
Change-Id: I1ad486c18f6dc5936d7ff8f31db116314a0edf88
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8311085
Reviewed-by: Yaron Friedman <yfriedman@chromium.org>
Reviewed-by: Jinsuk Kim <jinsukkim@chromium.org>
Bot-Commit: rubber-stamper@appspot.gserviceaccount.com <rubber-stamper@appspot.gserviceaccount.com>
Commit-Queue: Michael Thiessen <mthiesse@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1689141}
---
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/IntentHandler.java b/chrome/android/java/src/org/chromium/chrome/browser/IntentHandler.java
index c37dd549..00464284 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/IntentHandler.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/IntentHandler.java
@@ -1299,7 +1299,6 @@
private static @Nullable String extractUrlFromIntent(@Nullable Intent intent) {
if (intent == null) return null;
String url = getUrlFromVoiceSearchResult(intent);
- if (url == null) url = getUrlForCustomTab(intent);
if (url == null) url = getUrlForWebapp(intent);
if (url == null) url = getUrlFromShareIntent(intent);
if (url == null) url = getUrlForHandoff(intent);
@@ -1372,14 +1371,6 @@
return TemplateUrlServiceFactory.getForProfile(profile).getUrlForSearchQuery(text);
}
- private static @Nullable String getUrlForCustomTab(@Nullable Intent intent) {
- if (intent == null || intent.getData() == null) return null;
- Uri data = intent.getData();
- return TextUtils.equals(data.getScheme(), UrlConstants.CUSTOM_TAB_SCHEME)
- ? data.getQuery()
- : null;
- }
-
private static @Nullable String getUrlForWebapp(@Nullable Intent intent) {
if (intent == null || intent.getData() == null) return null;
Uri data = intent.getData();
diff --git a/components/embedder_support/android/java/src/org/chromium/components/embedder_support/util/UrlConstants.java b/components/embedder_support/android/java/src/org/chromium/components/embedder_support/util/UrlConstants.java
index 8dfeceb..0db9d99 100644
--- a/components/embedder_support/android/java/src/org/chromium/components/embedder_support/util/UrlConstants.java
+++ b/components/embedder_support/android/java/src/org/chromium/components/embedder_support/util/UrlConstants.java
@@ -21,7 +21,6 @@
public static final String CHROME_EXTENSION_SCHEME = "chrome-extension";
public static final String CHROME_NATIVE_SCHEME = "chrome-native";
public static final String CONTENT_SCHEME = "content";
- public static final String CUSTOM_TAB_SCHEME = "customtab";
public static final String DATA_SCHEME = "data";
public static final String DEVTOOLS_SCHEME = "devtools";
public static final String DISTILLER_SCHEME = "chrome-distiller";
Original Bug Report
Potential intent security check bypass loading internal URLs via CCT data URI rewrite desync
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A URL extraction desync exists between LaunchIntentDispatcher and CustomTabIntentDataProvider when launching Custom Tabs. When LaunchIntentDispatcher validates an incoming intent, scheme-gated extractors return a benign URL, but the intent’s data URI is subsequently rewritten while preserving unverified extras such as EXTRA_HANDOFF_URL. The resulting Custom Tab initializes without re-validating the intent, falling through to EXTRA_HANDOFF_URL and potentially allowing untrusted Android apps to navigate to privileged internal schemes like chrome://.
Affected files:
chrome/android/java/src/org/chromium/chrome/browser/LaunchIntentDispatcher.javachrome/android/java/src/org/chromium/chrome/browser/IntentHandler.javachrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabIntentDataProvider.javachrome/android/java/src/org/chromium/chrome/browser/customtabs/content/CustomTabIntentHandler.java
Estimated timestamp from git blame: 2026-04-16
Summary
When launching a Custom Tab via LaunchIntentDispatcher, security validation via IntentHandler.shouldIgnoreIntent() is performed on the original incoming Intent. When createCustomTabActivityIntent() prepares the launch intent for CustomTabActivity, it rewrites the intent’s data URI to the extracted benign URL while copying all intent extras verbatim. Because URL extraction in IntentHandler.extractUrlFromIntent() relies on a prioritized chain where earlier extractors (getUrlForCustomTab and getUrlForWebapp) are gated on specific schemes (customtab:// or webapp://), rewriting the data scheme to https:// causes CustomTabIntentDataProvider to fall through to getUrlForHandoff() (EXTRA_HANDOFF_URL), which is ungated and unverified. CustomTabActivity initial tab creation does not re-run shouldIgnoreIntent(), leading to a potential security check bypass.
Affected Files
chrome/android/java/src/org/chromium/chrome/browser/LaunchIntentDispatcher.javachrome/android/java/src/org/chromium/chrome/browser/IntentHandler.javachrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabIntentDataProvider.javachrome/android/java/src/org/chromium/chrome/browser/customtabs/content/CustomTabIntentHandler.java
Root Cause Analysis
-
Validation on Original Intent (
LaunchIntentDispatcher.java:285-292):launchCustomTabActivity()validates the untrusted incoming intent:if (IntentHandler.shouldIgnoreIntent(mIntent, mActivity, isCustomTab)) { return false; }IntentHandler.extractUrlFromIntent(intent)(IntentHandler.java:1299-1310) evaluatesgetUrlForCustomTab(intent)beforegetUrlForHandoff(intent):private static @Nullable String extractUrlFromIntent(@Nullable Intent intent) { if (intent == null) return null; String url = getUrlFromVoiceSearchResult(intent); if (url == null) url = getUrlForCustomTab(intent); // [Extractor 2] if (url == null) url = getUrlForWebapp(intent); // [Extractor 3] if (url == null) url = getUrlFromShareIntent(intent); // [Extractor 4] if (url == null) url = getUrlForHandoff(intent); // [Extractor 5] if (url == null) url = intent.getDataString(); ... }For
data = "customtab://x?https://benign.example", Extractor 2 (IntentHandler.java:1375-1381) matches the scheme and returns"https://benign.example". Because this URL is safe,IntentHandler.shouldIgnoreIntent()returnsfalse. -
Data Rewrite & Extra Propagation (
LaunchIntentDispatcher.java:197-210):createCustomTabActivityIntent()creates a copy of the intent carrying over all extras (includingEXTRA_HANDOFF_URL), but rewrites the data URI:String uriString = IntentHandler.getUrlFromIntent(intent); // "https://benign.example" Uri uri = Uri.parse(uriString); Intent newIntent = new Intent(intent); // Copies all extras if (ChromeFeatureList.isEnabled(ChromeFeatureList.CCT_DONT_OVERRIDE_INTENT_MIME_TYPE)) { newIntent.setDataAndType(uri, intent.getType()); // Data rewritten to https://... } else { newIntent.setAction(Intent.ACTION_VIEW); newIntent.setData(uri); } -
Extractor Fallthrough in CustomTabActivity (
CustomTabIntentDataProvider.java:1133-1148): WhenCustomTabActivityreceives the launch intent,resolveUrlToLoad()callsIntentHandler.getUrlFromIntent():- Extractor 2 (
getUrlForCustomTab) checksdata.getScheme() == "customtab"$\rightarrow$false. - Extractor 3 (
getUrlForWebapp) checksdata.getScheme() == "webapp"$\rightarrow$false. - Extractor 4 (
getUrlFromShareIntent) checksaction == ACTION_SEND$\rightarrow$false. - Extractor 5 (
getUrlForHandoff,IntentHandler.java:1391-1394) checksEXTRA_HANDOFF_URLwith no scheme check or sender verification $\rightarrow$ returns attacker-controlled URL (e.g."chrome://flags").
- Extractor 2 (
-
Unvalidated Navigation (
CustomTabIntentHandler.java:74-84/DefaultCustomTabIntentHandlingStrategy.java:89-95):CustomTabIntentHandler.handleInitialIntent()executes without invokingIntentHandler.shouldIgnoreIntent(), navigating directly to the URL returned bygetUrlToLoad()viatab.loadUrl().
Potential Reproduction Steps
(Note: These are suggested steps identified via static source code analysis; automated tooling does not execute code.)
- From an installed Android application, construct an explicit
Intent:Intent intent = new Intent(Intent.ACTION_VIEW); intent.setClassName("com.android.chrome", "com.google.android.apps.chrome.IntentDispatcher"); intent.setData(Uri.parse("customtab://x?https://benign.example")); intent.putExtra("org.chromium.chrome.browser.handoff_url", "chrome://flags"); Bundle bundle = new Bundle(); bundle.putBinder("android.support.customtabs.extra.SESSION", null); intent.putExtras(bundle); - Call
context.startActivity(intent). ChromeLauncherActivitydispatches toLaunchIntentDispatcher.launchCustomTabActivity().IntentHandler.shouldIgnoreIntent()verifies"https://benign.example"and allows the launch.createCustomTabActivityIntent()rewrites the data URI tohttps://benign.example, preservingEXTRA_HANDOFF_URL.CustomTabActivityextracts the URL to load, falling through togetUrlForHandoff(), which yields"chrome://flags".CustomTabActivityNavigationController.navigate()loads"chrome://flags"into the Custom Tab.
Suggested Fix
- Strip
IntentHandler.EXTRA_HANDOFF_URL(and other unverified extras not intended for external consumption) inLaunchIntentDispatcher.launchCustomTabActivity()orcreateCustomTabActivityIntent()before launchingCustomTabActivity. - Ensure
getUrlForHandoff()validates that the sender is trustworthy (IntentHandler.wasIntentSenderChrome()) before extractingEXTRA_HANDOFF_URL. - Alternatively, re-verify the resolved URL in
CustomTabIntentHandler.handleInitialIntent()orCustomTabIntentDataProviderusingIntentHandler.shouldIgnoreIntent()orExternalIntentUrlChecker.isUnsafeExternalIntentUrl().
Evaluated with Chrome root at commit: c6718ebbf2c677c6f63691e28c7801c563f6b19c
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.