CVE-2026-79254
Overview
Background
- Custom Tabs (CCT)
- an Android surface that lets an external app host a Chrome-rendered tab in its own task, exposing an “Open in browser” action that hands the current URL to a full browser.
- Implicit `Intent` resolution
- Android matches an implicit
Intentagainst installed activities’ intent filters, and an activity matches only if it declares every category theIntentcarries. - `Intent.CATEGORY_BROWSABLE`
- the category a component must declare to be considered a safe target for web URLs launched from untrusted contexts; browsers declare it, most non-browser handlers do not.
- `FLAG_ACTIVITY_NEW_TASK` / `EXTRA_FROM_OPEN_IN_BROWSER`
- the flag and extra set on the “Open in browser”
Intentso it launches in a new task and is recognized downstream as originating from this action.
Root Cause Analysis
When a Custom Tab user chose “Open in browser,” the code built an implicit Intent(Intent.ACTION_VIEW, Uri.parse(url)) and dispatched it (via detachAndStartReparenting or startActivity) without adding Intent.CATEGORY_BROWSABLE. Because the Intent carried only ACTION_VIEW plus a URL, Android’s resolution could match components that register for the scheme but are not browsable-safe web handlers, so the URL reference could be resolved to an unintended activity rather than an actual browser. This is the “incorrect reference resolution” logic error: the invariant that a web URL surfaced from an untrusted CCT context should only be handed to browsable (browser-grade) targets was not enforced.
The fix adds intent.addCategory(Intent.CATEGORY_BROWSABLE), so resolution is constrained to activities whose intent filters declare CATEGORY_BROWSABLE, matching the categorization the OS uses for normal web-link handling. The accompanying tests capture the dispatched Intent and assert intent.hasCategory(Intent.CATEGORY_BROWSABLE) on both the reparenting and startActivity paths.
ACTION_VIEW URL Intent from the CCT “Open in browser” entry point without the CATEGORY_BROWSABLE category, letting non-browsable components become eligible resolution targets; the fix adds that category so the Intent is only resolvable by proper browser handlers.Attack Path
- Host a Custom Tab A malicious or complicit app launches a Custom Tab pointing at a URL under attacker influence and the user reaches the “Open in browser” action.
- Trigger the flawed dispatch
Invoking
openCurrentUrlInBrowser()buildsIntent(ACTION_VIEW, url)withFLAG_ACTIVITY_NEW_TASKandEXTRA_FROM_OPEN_IN_BROWSERbut noCATEGORY_BROWSABLE, then dispatches it. - Capture via non-browsable filter
An installed component that registers for the URL’s scheme without declaring
CATEGORY_BROWSABLEcan be matched during resolution, so the URL reference is resolved to that component instead of a browser (inferred). - Consequence
The resolved component receives the URL (and the
EXTRA_FROM_OPEN_IN_BROWSERtrust marker), diverging from the intended “open in a real browser” behavior (inferred).
Impact Assessment
CATEGORY_BROWSABLE is present and reachable; the advisory classifies this as a logic error (incorrect reference resolution in CustomTabs) and does not report in-the-wild exploitation. Any escalation beyond misdirected intent resolution is (inferred).Files Changed
chrome/android/java/src/org/chromium/chrome/browser/customtabs/content/CustomTabActivityNavigationController.javachrome/android/junit/src/org/chromium/chrome/browser/customtabs/content/CustomTabActivityNavigationControllerTest.java
Audit Directions
- Implicit `ACTION_VIEW` URL intentsAudit every site where Chrome constructs an implicit
ACTION_VIEWIntentfrom a web URL and dispatches it, checking thatCATEGORY_BROWSABLEis set so non-browsable components cannot intercept. - Entry-point parityWhen one entry point (e.g., an earlier CL like crrev.com/c/8076861) is hardened, grep for sibling entry points building the same kind of
Intent(hereCustomTabActivityNavigationController) that were missed. - Trust-marker extras on under-constrained intentsFlag intents carrying trust markers such as
EXTRA_FROM_OPEN_IN_BROWSERwhose target set is not tightly restricted, since a mis-resolved target inherits that implied trust.
Patch
From 2ba952e0154b8e9d6d7b1e755f82b576255a541e Mon Sep 17 00:00:00 2001 From: Sinan Sahin <[email protected]> Date: Thu, 16 Jul 2026 10:30:52 -0700 Subject: [PATCH] Add CATEGORY_BROWSABLE to CCT 'Open in browser' intent Similar to https://crrev.com/c/8076861, but this time the entry point is CustomTabActivityNavigationController. Bug: 534556413 Change-Id: I68c36ea42eb02402fbabf3114936bcfe7fb8d8fe Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8103622 Commit-Queue: Sinan Sahin <[email protected]> Reviewed-by: Michael Thiessen <[email protected]> Cr-Commit-Position: refs/heads/main@{#1663278} --- diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/content/CustomTabActivityNavigationController.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/content/CustomTabActivityNavigationController.java index ef741b7..657e50e 100644 --- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/content/CustomTabActivityNavigationController.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/content/CustomTabActivityNavigationController.java @@ -366,6 +366,7 @@ assertUrlNotNullForOpenInBrowser(url, tab); Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + intent.addCategory(Intent.CATEGORY_BROWSABLE); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra(IntentHandler.EXTRA_FROM_OPEN_IN_BROWSER, true); ResolveInfo resolveInfo = DefaultBrowserInfo.getDefaultWebBrowserInfo(); diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/content/CustomTabActivityNavigationControllerTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/content/CustomTabActivityNavigationControllerTest.java index 362d2c26..298180f9 100644 --- a/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/content/CustomTabActivityNavigationControllerTest.java +++ b/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/content/CustomTabActivityNavigationControllerTest.java @@ -22,6 +22,7 @@ import android.content.Context; import android.content.ContextWrapper; +import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.Build; @@ -248,7 +249,10 @@ ExternalNavigationDelegateImpl.setWillChromeHandleIntentHookForTesting(intent -> true); mNavigationController.openCurrentUrlInBrowser(); verify(env.activity, never()).startActivity(any()); - verify(mTabController).detachAndStartReparenting(any(), any(), any()); + ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class); + verify(mTabController).detachAndStartReparenting(intentCaptor.capture(), any(), any()); + Intent intent = intentCaptor.getValue(); + assertTrue(intent.hasCategory(Intent.CATEGORY_BROWSABLE)); } @Test @@ -284,7 +288,10 @@ ExternalNavigationDelegateImpl.setWillChromeHandleIntentHookForTesting(intent -> false); mNavigationController.openCurrentUrlInBrowser(); verify(mTabController, never()).detachAndStartReparenting(any(), any(), any()); - verify(env.activity).startActivity(any(), any()); + ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class); + verify(env.activity).startActivity(intentCaptor.capture(), any()); + Intent intent = intentCaptor.getValue(); + assertTrue(intent.hasCategory(Intent.CATEGORY_BROWSABLE)); verify(mFinishHandler).onFinish(FinishReason.OPEN_IN_BROWSER, true); }
Original Bug Report
Potential CATEGORY_BROWSABLE bypass when opening Chrome Custom Tab URLs in browser
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: When a user selects ‘Open in browser’ in a Chrome Custom Tab (CCT), Chrome dispatches an implicit ACTION_VIEW intent with the active URL but fails to set Intent.CATEGORY_BROWSABLE. Under specific system conditions where no default browser has been selected, this intent can resolve to and launch non-browsable activities of other installed apps. This allows a web origin to potentially bypass the CATEGORY_BROWSABLE security boundary and deliver arbitrary deep-links to internal, non-web-reachable activities.
Affected files:
chrome/android/java/src/org/chromium/chrome/browser/customtabs/content/CustomTabActivityNavigationController.java
Estimated timestamp from git blame: 2016-01-14
Description
There is a potential security boundary bypass in Chrome Custom Tabs (CCT) on Android. When a user selects “Open in browser” from the CCT overflow menu or custom toolbar buttons, Chrome dispatches an implicit ACTION_VIEW intent containing the current tab’s URL. However, this intent does not contain the Intent.CATEGORY_BROWSABLE category.
Under standard Android intent resolution rules, an implicit intent with no categories can match any activity whose intent-filter contains <category android:name="android.intent.category.DEFAULT" />, regardless of whether it lacks CATEGORY_BROWSABLE. App developers omit CATEGORY_BROWSABLE specifically to prevent web-to-app launches and secure their internal deep-link handlers. By dispatching a web-derived URL in an intent without CATEGORY_BROWSABLE, Chrome can accidentally route arbitrary attacker-controlled web URLs to these internal, non-web-reachable activities.
Root Cause and Analysis
In chrome/android/java/src/org/chromium/chrome/browser/customtabs/content/CustomTabActivityNavigationController.java, the openCurrentUrlInBrowser() method prepares the intent:
// Line 368-370
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(IntentHandler.EXTRA_FROM_OPEN_IN_BROWSER, true);
Note that no category is added to the intent.
If multiple browsers are installed and no default is set on the device, DefaultBrowserInfo.getDefaultWebBrowserInfo() returns the system’s ResolverActivity (package name "android"). The package check on line 373-376 restricts the package to "android", fails to resolve the https scheme for that package, and clears the package restriction:
ResolveInfo resolveInfo = DefaultBrowserInfo.getDefaultWebBrowserInfo();
if (resolveInfo != null) {
intent.setPackage(resolveInfo.activityInfo.packageName);
// crbug.com/40801270
if (intent.resolveActivity(mActivity.getPackageManager()) == null) {
intent.setPackage(null);
}
}
As a result, the intent is dispatched as a fully implicit intent with setPackage(null). Since the intent target is external and willChromeHandleIntent returns false, execution falls through to the final fallback block:
// Line 425-427
} else if (PackageManagerUtils.canResolveActivity(intent)) {
mActivity.startActivity(intent, startActivityOptions);
finish(FinishReason.OPEN_IN_BROWSER);
}
The intent is then dispatched globally without CATEGORY_BROWSABLE, matching and potentially opening non-browsable activities.
This behavior is an asymmetric contrast to the similar menu context-path in chrome/android/java/src/org/chromium/chrome/browser/tab/TabContextMenuItemDelegate.java which correctly enforces the security restriction:
// Line 565-566
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url.getSpec()));
intent.addCategory(Intent.CATEGORY_BROWSABLE); // Enforced category
Potential Trigger Steps
Note: The following are suggested/potential steps to demonstrate the issue, as our tooling agent does not currently have the capability to execute code or run interactive proofs.
- Prepare an Android device with at least two web browsers installed, and with no default browser chosen in system settings.
- Install a test application containing an activity that handles
https://victim-host.exampleand defines theCATEGORY_DEFAULTcategory but explicitly omitsCATEGORY_BROWSABLE. - Launch a Chrome Custom Tab to
https://attacker.test/from any third-party app. - Within the CCT, navigate to
https://victim-host.example/sensitive_action?command=evil. - Tap the CCT menu or toolbar option to “Open in browser”.
- Observe that the non-browsable activity is either presented in the chooser or launched directly, bypassing the
CATEGORY_BROWSABLErestriction.
Suggested Fix
Explicitly sanitize the intent or add Intent.CATEGORY_BROWSABLE in CustomTabActivityNavigationController.openCurrentUrlInBrowser() prior to checking resolution and dispatching, mirroring the fix implemented in TabContextMenuItemDelegate:
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
intent.addCategory(Intent.CATEGORY_BROWSABLE);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Evaluated with Chrome root at commit: b5b015ea5f690560237d1f0cff1405844cd12b8d
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.