CVE-2025-3068
Overview
Background
- Intent URI / external navigation
- On Android, a web page can request that Chrome hand off a navigation to another app via an
intent:(or scheme-bearing) URI, whichExternalNavigationHandlerparses into anIntentand dispatches. - `Intent` scheme
- The URI scheme carried in the intent’s data (
intent.getData().getScheme()), which Android treats as case-sensitive, sow3irDandw3irdare distinct scheme strings. - `normalizeScheme()`
- The Android
Urimethod that returns the URI with its scheme lower-cased, used here to canonicalize the intent’s data before dispatch. - `intent.setSelector(null)`
- A pre-existing hardening step in the same code that clears the intent selector so an intent cannot bypass intent filters (crbug.com/1254422), i.e. the surrounding code already sanitizes the outgoing intent.
Root Cause Analysis
When preparing an external intent for dispatch, the handler cleared the selector but passed the intent’s data through with its scheme exactly as supplied by the page, and the removed test comment states the old expectation explicitly: “Schemes on Android are case-sensitive, so ensure the scheme is passed through as-is,” with the intent’s scheme preserved as w3irD. Because Android compares schemes case-sensitively, any allow/deny or filter logic keyed on a lower-cased scheme name could be evaded by supplying a mixed-case scheme (e.g. W3IRD vs w3ird), so a page-controlled intent could reach an app or resolution path that a canonical-case comparison would have excluded.
The fix, gated behind kLowerCaseIntentSchemes / LOWER_CASE_INTENT_SCHEMES, adds a block that, when the feature is enabled and intent.getData() is non-null, rewrites the intent via intent.setDataAndType(intent.getData().normalizeScheme(), intent.getType()) so the scheme is normalized to lower case before dispatch. The updated test now asserts the scheme is w3ird, confirming the intent that leaves Chrome carries a canonicalized scheme, closing the case-sensitivity gap; the feature flag exists as a kill switch in case normalization breaks legitimate apps.
normalizeScheme() before the intent is dispatched.Attack Path
- Craft a mixed-case intent URI A malicious web page constructs an external navigation whose intent data uses a non-canonical (mixed-case) scheme designed to differ from the lower-cased form used by downstream checks.
- Trigger external navigation
The page navigates so Chrome routes the request through
ExternalNavigationHandler, which builds theIntentfrom the URI. - Bypass scheme-keyed handling
Pre-patch, the intent’s data scheme is passed through as-is after
setSelector(null), so any scheme-based allow/deny/routing that assumes canonical casing is evaluated against the un-normalized scheme and can be evaded(inferred). - Intent dispatched with attacker casing
The intent is launched with the original scheme casing, potentially resolving to an app or handling path the browser’s checks intended to exclude. The specific downstream app/resource reached is
(inferred).
Impact Assessment
(inferred). The fix ships enabled by default (M136) behind the LowerCaseIntentSchemes kill switch, and requires the user/page to initiate an external navigation carrying an intent with data.Files Changed
components/external_intents/android/external_intents_features.cccomponents/external_intents/android/external_intents_features.hcomponents/external_intents/android/java/src/org/chromium/components/external_intents/ExternalIntentsFeatures.javacomponents/external_intents/android/java/src/org/chromium/components/external_intents/ExternalNavigationHandler.javacomponents/external_intents/android/javatests/src/org/chromium/components/external_intents/ExternalNavigationHandlerTest.java
Audit Directions
- Case-sensitive vs case-insensitive scheme handlingAudit other places that compare, filter, or route on URI/intent schemes to ensure both sides use the same canonical (lower-cased) form, since Android schemes are case-sensitive while many checks assume lower case.
- Page-supplied data forwarded to Android IPCReview paths that build
Intents from web-controlled URIs and pass data/type/action/component through without normalization, following the existingsetSelector(null)hardening pattern. - Normalization gated behind flagsWhen a fix normalizes untrusted input behind a kill-switch feature, verify no earlier check relies on the un-normalized value and that the default-enabled path is the safe one.
Patch
From 720a32bd47d3188bbf824743147bc17de31de8c4 Mon Sep 17 00:00:00 2001 From: Ken Buchanan <[email protected]> Date: Thu, 20 Mar 2025 11:10:50 -0700 Subject: [PATCH] Normalize intent URIs for external navigations This is a follow-up to https://chromium-review.googlesource.com/c/chromium/src/+/6367258. Intent URIs will now be normalized to lower case. There is a flag to act as a kill switch if any breakage results. Bug: 401823929 Change-Id: I9346c8ed2f80469c81bebfe192d9a88d1dfebcae Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6367784 Commit-Queue: Ken Buchanan <[email protected]> Reviewed-by: Michael Thiessen <[email protected]> Cr-Commit-Position: refs/heads/main@{#1435559} --- diff --git a/components/external_intents/android/external_intents_features.cc b/components/external_intents/android/external_intents_features.cc index 6c81905..afae110f 100644 --- a/components/external_intents/android/external_intents_features.cc +++ b/components/external_intents/android/external_intents_features.cc @@ -26,7 +26,7 @@ // Array of features exposed through the Java ExternalIntentsFeatures API. const base::Feature* const kFeaturesExposedToJava[] = { &kExternalNavigationDebugLogs, &kBlockFrameRenavigations, - &kBlockIntentsToSelf}; + &kBlockIntentsToSelf, &kLowerCaseIntentSchemes}; } // namespace @@ -44,6 +44,11 @@ "BlockIntentsToSelf", base::FEATURE_ENABLED_BY_DEFAULT); +// Enabled by default in M136. +BASE_FEATURE(kLowerCaseIntentSchemes, + "LowerCaseIntentSchemes", + base::FEATURE_ENABLED_BY_DEFAULT); + static jlong JNI_ExternalIntentsFeatures_GetFeature(JNIEnv* env, jint ordinal) { return reinterpret_cast<jlong>(kFeaturesExposedToJava[ordinal]); } diff --git a/components/external_intents/android/external_intents_features.h b/components/external_intents/android/external_intents_features.h index 1c7535d..9644a25 100644 --- a/components/external_intents/android/external_intents_features.h +++ b/components/external_intents/android/external_intents_features.h @@ -13,6 +13,9 @@ BASE_DECLARE_FEATURE(kBlockFrameRenavigations); BASE_DECLARE_FEATURE(kBlockIntentsToSelf); +// When enabled, forces intent schemes to be lower case. +BASE_DECLARE_FEATURE(kLowerCaseIntentSchemes); + } // namespace external_intents #endif // COMPONENTS_EXTERNAL_INTENTS_ANDROID_EXTERNAL_INTENTS_FEATURES_H_ diff --git a/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalIntentsFeatures.java b/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalIntentsFeatures.java index e2e4578..fab0065 100644 --- a/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalIntentsFeatures.java +++ b/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalIntentsFeatures.java @@ -23,6 +23,7 @@ public static final String EXTERNAL_NAVIGATION_DEBUG_LOGS_NAME = "ExternalNavigationDebugLogs"; public static final String BLOCK_FRAME_RENAVIGATIONS_NAME = "BlockFrameRenavigations3"; public static final String BLOCK_INTENTS_TO_SELF_NAME = "BlockIntentsToSelf"; + public static final String LOWER_CASE_INTENT_SCHEMES_NAME = "LowerCaseIntentSchemes"; public static final ExternalIntentsFeatures EXTERNAL_NAVIGATION_DEBUG_LOGS = new ExternalIntentsFeatures(0, EXTERNAL_NAVIGATION_DEBUG_LOGS_NAME); @@ -33,6 +34,9 @@ public static final ExternalIntentsFeatures BLOCK_INTENTS_TO_SELF = new ExternalIntentsFeatures(2, BLOCK_INTENTS_TO_SELF_NAME); + public static final ExternalIntentsFeatures LOWER_CASE_INTENT_SCHEMES = + new ExternalIntentsFeatures(3, LOWER_CASE_INTENT_SCHEMES_NAME); + private final int mOrdinal; private ExternalIntentsFeatures(int ordinal, String name) { diff --git a/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalNavigationHandler.java b/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalNavigationHandler.java index c7ebed0..58e81d3 100644 --- a/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalNavigationHandler.java +++ b/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalNavigationHandler.java @@ -1824,6 +1824,12 @@ // Intent Selectors allow intents to bypass the intent filter and potentially send apps URIs // they were not expecting to handle. https://crbug.com/1254422 intent.setSelector(null); + + // Intent schemes should be normalized to lower case. https://crbug.com/401823929 + if (ExternalIntentsFeatures.LOWER_CASE_INTENT_SCHEMES.isEnabled() + && intent.getData() != null) { + intent.setDataAndType(intent.getData().normalizeScheme(), intent.getType()); + } } /** diff --git a/components/external_intents/android/javatests/src/org/chromium/components/external_intents/ExternalNavigationHandlerTest.java b/components/external_intents/android/javatests/src/org/chromium/components/external_intents/ExternalNavigationHandlerTest.java index ce43bf8..ae11749 100644 --- a/components/external_intents/android/javatests/src/org/chromium/components/external_intents/ExternalNavigationHandlerTest.java +++ b/components/external_intents/android/javatests/src/org/chromium/components/external_intents/ExternalNavigationHandlerTest.java @@ -1999,8 +1999,8 @@ .expecting( OverrideUrlLoadingResultType.OVERRIDE_WITH_EXTERNAL_INTENT, START_OTHER_ACTIVITY); - // Schemes on Android are case-sensitive, so ensure the scheme is passed through as-is. - Assert.assertEquals("w3irD", mUrlHandler.mStartActivityIntent.getScheme()); + // Intent schemes should be normalized. + Assert.assertEquals("w3ird", mUrlHandler.mStartActivityIntent.getScheme()); } @Test
Original Bug Report
intent:// can bypass fido:/ URI bock (see: 370482421)
VULNERABILITY DETAILS It’s possbile to bypass the fix for 370482421 by using a intent URI.
VERSION Chrome Version: 133.0.6943.137 stable Operating System: Pixel 7 Pro Android 15 March security updates.
REPRODUCTION CASE See https://issues.chromium.org/issues/370482421 for the original discussion. TL;DR: A website should not be allowed to open FIDO:/ URLs on mobile devices.
The issue is that the original fix was never tested on intent:// URLs. The original fix is a case sensitive check on the intent, it checks for “fido” and not “FIDO”. I have attached a html file whit a POC link that will start FIDO activity when clicked. The file also includes links that are blocked and the console output when clicked.
The fix seams to be fairly straight forward. Use a case insensitive match like what “hasFileSchemeInIntentURI” dose. See attached diff.
CREDIT INFORMATION Externally reported security bugs may appear in Chrome release notes. If this bug is included, how would you like to be credited? Reporter credit: [goes here]