CVE-2026-79222
Overview
Background
- Custom Tabs
- an Android feature letting a host app open web content in a Chrome-backed activity, with certain privileges (e.g. first-party behaviors) reserved for verified origins.
- `CustomTabsConnection.isFirstPartyOriginForIntent`
- the check that decides whether the origin associated with a Custom Tabs
Intentis a verified first-party origin for the calling session. - `intent.getData()`
- the raw data URI directly attached to an Android
Intent, which is not necessarily the URL Chrome will actually navigate to. - `IntentHandler.getUrlFromIntent(intent)`
- Chrome’s canonical resolver that derives the URL Chrome will actually load from an intent, accounting for extras and normalization beyond the bare data URI.
Root Cause Analysis
isFirstPartyOriginForIntent in CustomTabsConnection.java derived the origin to authorize from Origin.create(intent.getData()) — the intent’s raw data URI. The URL Chrome actually resolves and navigates to is computed separately by IntentHandler.getUrlFromIntent(intent), so the origin being verified could differ from the origin actually loaded. This mismatch means the first-party check could be satisfied for one origin (from getData()) while navigation proceeds to a different origin, granting first-party privileges to an unintended origin.
The fix computes String url = IntentHandler.getUrlFromIntent(intent), returns false if it is null, and builds the Origin from that resolved URL, so the authorization decision is made against the same URL Chrome will navigate to. The test files are updated in parallel to construct verification overrides from IntentHandler.getUrlFromIntent(...) rather than intent.getData(), aligning the tests with the corrected origin source.
getData() URI instead of the URL Chrome actually resolves and loads, letting the verified origin diverge from the navigated origin; the fix derives the origin from IntentHandler.getUrlFromIntent(intent) so the check matches the real navigation target.Attack Path
- Craft a Custom Tabs intent
A malicious or colluding app builds a Custom Tabs
Intentfor a session, arranging forintent.getData()to point at an origin it can pass first-party verification for. - Cause divergent resolution
Structure the intent so that
IntentHandler.getUrlFromIntent(intent)resolves to a different URL/origin than the rawgetData()value (inferred). - Pass the origin check
Pre-patch,
isFirstPartyOriginForIntentbuilds the origin fromgetData()andisFirstPartyOriginForSessionreturns true for that origin. - Obtain first-party behavior on a different origin
Chrome navigates to the origin from
getUrlFromIntent, so the first-party privileges granted by the (mismatched) check apply to an origin that was never actually verified.
Impact Assessment
Files Changed
chrome/android/javatests/src/org/chromium/chrome/browser/IntentHandlerNativeTest.javachrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsConnection.javachrome/android/javatests/src/org/chromium/chrome/browser/customtabs/CustomTabActivityTest.java
Audit Directions
- Origin from raw intent dataSearch Android/Custom Tabs code for
Origin.create(intent.getData())or similar uses of raw intent URIs in security decisions, and confirm they use the resolved navigation URL instead. - Check-target vs. use-target mismatchAudit authorization checks where the value validated is computed differently from the value later acted upon (verify one URL, navigate another); ensure both come from a single canonical resolver.
- Null-resolution handlingReview resolvers like
getUrlFromIntentfor callers that assume a non-null result, since anullresolved URL must fail closed rather than fall through to a permissive default.
Patch
From 42fde8f7473ab60770518af2fef27b1782522ed7 Mon Sep 17 00:00:00 2001
From: Mohamed Adel <adelm@google.com>
Date: Mon, 06 Jul 2026 12:33:43 -0700
Subject: [PATCH] Fix origin check for Custom Tabs intent resolution
Update `CustomTabsConnection.isFirstPartyOriginForIntent` and related
tests to derive the origin from the actual resolved navigation URL using
`IntentHandler.getUrlFromIntent(intent)` rather than strictly from
`intent.getData()`.
Fixed: 496195129
Change-Id: I2b1037c5af643df9831052ba67979edbdbc10fc8
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8033242
Reviewed-by: Theresa Wellington <twellington@chromium.org>
Reviewed-by: Sinan Sahin <sinansahin@google.com>
Auto-Submit: Moe Adel <adelm@google.com>
Commit-Queue: Theresa Wellington <twellington@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1657385}
---
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 bf36da7d..a8e73f5 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
@@ -1010,7 +1010,10 @@
SessionHolder<?> session = SessionHolder.getSessionHolderFromIntent(intent);
if (session == null) return false;
- Origin origin = Origin.create(intent.getData());
+ String url = IntentHandler.getUrlFromIntent(intent);
+ if (url == null) return false;
+
+ Origin origin = Origin.create(url);
if (origin == null) return false;
return mClientManager.isFirstPartyOriginForSession(session, origin);
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/IntentHandlerNativeTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/IntentHandlerNativeTest.java
index 48b0a4e..938a68dd 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/IntentHandlerNativeTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/IntentHandlerNativeTest.java
@@ -186,7 +186,7 @@
() ->
ChromeOriginVerifier.addVerificationOverride(
"app1",
- Origin.create(headersIntent.getData()),
+ Origin.create(IntentHandler.getUrlFromIntent(headersIntent)),
CustomTabsService.RELATION_USE_AS_ORIGIN));
String extraHeaders = IntentHandler.getExtraHeadersFromIntent(headersIntent);
@@ -219,7 +219,7 @@
() ->
ChromeOriginVerifier.addVerificationOverride(
"app2",
- Origin.create(headersIntent.getData()),
+ Origin.create(IntentHandler.getUrlFromIntent(headersIntent)),
CustomTabsService.RELATION_USE_AS_ORIGIN));
String extraHeaders = IntentHandler.getExtraHeadersFromIntent(headersIntent);
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/CustomTabActivityTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/CustomTabActivityTest.java
index 9db857f..0b18fc73d 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/CustomTabActivityTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/CustomTabActivityTest.java
@@ -362,7 +362,7 @@
() ->
ChromeOriginVerifier.addVerificationOverride(
"app1",
- Origin.create(intent.getData()),
+ Origin.create(IntentHandler.getUrlFromIntent(intent)),
CustomTabsService.RELATION_USE_AS_ORIGIN));
final var session = warmUpAndLaunchUrlWithSession(intent);
Original Bug Report
Potential CCT first-party header gate bypass via intent.getData() / navigation-URL divergence
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A discrepancy in how Chrome extracts a navigation URL versus how it verifies a first-party origin allows an attacker to send non-safelisted headers to arbitrary cross-origin sites in a Custom Tab. By placing their verified origin in intent.getData() and the target URL in EXTRA_TEXT, an attacker can bypass security checks and facilitate CSRF or session-based attacks.
Affected files:
chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsConnection.javachrome/android/java/src/org/chromium/chrome/browser/IntentHandler.javachrome/android/java/src/org/chromium/chrome/browser/customtabs/content/CustomTabActivityNavigationController.javachrome/android/java/src/org/chromium/chrome/browser/LaunchIntentDispatcher.java
Estimated timestamp from git blame: 2024-12-23
Overview
A potential security vulnerability exists in Chrome for Android where the Digital Asset Links (DAL) verification for Custom Tabs can be bypassed. This check is intended to restrict ’non-safelisted’ HTTP headers (such as Authorization or X-Csrf-Token) so they can only be sent to an app’s own verified domain. Due to a discrepancy in how the navigation URL is extracted versus how the DAL origin is determined, an attacker-controlled app can attach arbitrary sensitive headers to top-level cross-origin navigations.
Vulnerability Details
The CustomTabsConnection.isFirstPartyOriginForIntent() method determines if an app is a ‘first-party’ for a given intent’s origin using DAL verification. It derives the origin strictly from intent.getData():
Origin origin = Origin.create(intent.getData());
However, the actual navigation URL is resolved via IntentHandler.getUrlFromIntent(), which delegates to extractUrlFromIntent(). For certain intents, like ACTION_SEND with type text/plain, Chrome prioritizes fields like Intent.EXTRA_TEXT over intent.getData() for the navigation destination.
During early navigation processing (HiddenTabHolder.startEarlynavigation), Chrome uses the original intent to both extract the navigation URL and attach headers. Because the DAL check only looks at intent.getData(), an attacker can point intent.getData() to their own verified origin while pointing EXTRA_TEXT to the victim’s URL. The check passes, the CORS safelist filter is skipped, and the malicious headers are attached to the hidden tab’s request to the victim URL.
Subsequently, LaunchIntentDispatcher creates a sanitized launchIntent with its .setData() overwritten to the victim URL to start CustomTabActivity. In CustomTabActivityTabController.finishNativeInitialization, Chrome attempts to cancel any speculation if extra headers are present by calling IntentHandler.getExtraHeadersFromIntent(launchIntent). Because launchIntent.getData() is now the victim URL, the DAL check fails and the function returns null. This causes the cancellation check to fail, and the hidden tab successfully commits the cross-origin navigation with the injected headers.
Note: These are potential steps, as this analysis was performed by an AI agent (Fortify) without executing code to verify a working proof of concept.
Suggested Attacker Steps
- Setup: The attacker creates an Android app and completes DAL verification for an origin they control (e.g.,
https://attacker.example). - Session: The attacker app calls
CustomTabsClient.warmup()andCustomTabsClient.newSession(), verifying their relationship tohttps://attacker.exampleto ensure the DAL relationship is cached. - Intent Crafting: The app crafts an
Intent.ACTION_SENDintent with MIME typetext/plain. - Target and Origin: The app sets the intent’s data URI to their verified origin:
intent.setData(Uri.parse("https://attacker.example")). It sets the actual target victim URL in the extra text:intent.putExtra(Intent.EXTRA_TEXT, "https://victim.example/sensitive-action"). - Malicious Headers: The app bundles non-safelisted headers into the intent using the
Browser.EXTRA_HEADERSbundle (e.g.,Authorization: Bearer attacker_token). - Dispatch: The app launches the intent. Chrome’s early navigation mechanism processes the original intent, validates
intent.getData(), and launches a hidden tab tohttps://victim.examplewith the injected headers.
Suggested Fix
Modify CustomTabsConnection.isFirstPartyOriginForIntent(Intent intent) (and any similar checks) to derive the origin from the actual resolved navigation URL rather than strictly intent.getData(). Specifically, use IntentHandler.getUrlFromIntent(intent) to determine the origin being navigated to, ensuring the DAL check is performed against the true destination of the Custom Tab.
Evaluated with Chrome root at commit: bb48272cafb7e24c93f55ef40da398cd206ee651
Results 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. Please feel free to reach out to me if you have concerns or feedback.