CVE-2025-6428
Overview
Background
- `RedirectData.fallbackUrl`
- The URL Firefox navigates to when the targeted app cannot handle the intent. It drives where the user actually lands.
- `browser_fallback_url` / `afl` / `link`
browser_fallback_urlis the standard Android intent extra for a fallback;aflandlinkwere additionally read as query-string parameters of the URL itself.- `getHierarchicalUrl()`
- The removed helper that derived a fallback URL by reading the
aflorlinkquery parameter of the incoming URL, falling back to the intent’sbrowser_fallback_urlextra.
Root Cause Analysis
When intercepting an app-link redirect, AppLinksUseCases populated RedirectData.fallbackUrl by calling url.getHierarchicalUrl(). That helper first looked at the query string of the URL being navigated — getQueryParameter("afl") then getQueryParameter("link") — and only if those were absent consulted the intent’s browser_fallback_url extra. The security flaw is that the fallback destination was taken from attacker-controllable query parameters of the originating URL: a link like …?link=https://evil.example (or ?afl=…) caused Firefox for Android to navigate to that arbitrary URL instead of the intended destination, i.e. an open-redirect that enables convincing phishing because the user clicked a link that appeared to point elsewhere.
The patch removes the entire mechanism: it deletes the PARAMS_ANDROID_FALLBACK_LINK = "afl" and PARAMS_FALLBACK_LINK = "link" constants, deletes getHierarchicalUrl(), and sets fallbackUrl = null in the returned RedirectData. The test suite is rewritten to assert the opposite of the old behavior — assertFalse(redirect.hasFallback()), assertNull(redirect.fallbackUrl) — including for the marketplace and Play-Store cases, confirming the intended fix is that query-string-derived fallbacks are no longer honored at all. The advisory (CWE-601, open redirect) and the diff agree: the root cause is trusting a user/attacker-supplied URL from the link’s own query parameters as the navigation fallback.
afl/link query parameters of the incoming URL, so a crafted link redirected the browser to an arbitrary URL; the fix stops honoring query-string fallbacks entirely (fallbackUrl = null).Attack Path
- Craft link
Attacker builds an app-scheme/
intent:link whose query string containslink=orafl=pointing at an attacker-chosen phishing URL. (Firefox for Android only) - Lure The victim taps the link, which appears associated with a legitimate app/destination. (UI:R)
- Interception
AppLinksUseCases.interceptedAppLinkRedirectprocesses it; the targeted app is unavailable or declines, so a fallback is used. - Follow attacker URL
getHierarchicalUrl()reads thelink/aflquery parameter and sets it asfallbackUrl, so Firefox navigates there instead of the correct URL. - Primitive Open redirect to an attacker-controlled page, enabling spoofing/phishing against the user.
Impact Assessment
Files Changed
mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/AppLinksUseCases.ktmobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksUseCasesTest.kt
Audit Directions
- Other intent-extra consumersReview remaining use of
EXTRA_BROWSER_FALLBACK_URLand any code that still reads URLs out of intent extras/query parameters for navigation targets. - Redirect/navigation from untrusted parametersAudit other Android navigation paths (custom tabs, deep links) that build a destination from query-string values.
- Marketplace/Play-Store fallbackConfirm the removal did not reintroduce a different attacker-influenced fallback in the marketplace-intent branch.
Patch
diff --git a/mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/AppLinksUseCases.kt b/mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/AppLinksUseCases.kt
index a993e652fbb..a8b97bcc5af 100644
--- a/mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/AppLinksUseCases.kt
+++ b/mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/AppLinksUseCases.kt
@@ -26,8 +26,6 @@ import java.net.URISyntaxException
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal const val EXTRA_BROWSER_FALLBACK_URL = "browser_fallback_url"
-internal const val PARAMS_ANDROID_FALLBACK_LINK = "afl" // From https://firebase.google.com/docs/dynamic-links/create-manually
-internal const val PARAMS_FALLBACK_LINK = "link"
private const val MARKET_INTENT_URI_PACKAGE_PREFIX = "market://details?id="
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
@@ -202,24 +200,12 @@ class AppLinksUseCases(
return RedirectData(
appIntent = appIntent,
- fallbackUrl = url.getHierarchicalUrl(),
+ fallbackUrl = null,
marketplaceIntent = marketplaceIntent,
resolveInfo = resolveInfo,
)
}
- private fun String.getHierarchicalUrl(): String? {
- val fallbackUrlFromUrlParams = this.toUri()
- .takeIf { it.isHierarchical }
- ?.let { uriWithParams ->
- uriWithParams.getQueryParameter(PARAMS_ANDROID_FALLBACK_LINK)
- ?: uriWithParams.getQueryParameter(PARAMS_FALLBACK_LINK)
- }
-
- return fallbackUrlFromUrlParams ?: safeParseUri(this, Intent.URI_INTENT_SCHEME)
- ?.getStringExtra(EXTRA_BROWSER_FALLBACK_URL)
- }
-
private fun isPlayStoreURL(url: String): Boolean {
val playStoreUrlRegex = Regex("https?://play\\.google\\.com/store/.*")
return url.matches(playStoreUrlRegex)
diff --git a/mobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksUseCasesTest.kt b/mobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksUseCasesTest.kt
index 0a963b27b8a..b0b37af15da 100644
--- a/mobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksUseCasesTest.kt
+++ b/mobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksUseCasesTest.kt
@@ -154,7 +154,7 @@ class AppLinksUseCasesTest {
val redirect = subject.interceptedAppLinkRedirect(appIntentWithPackageAndFallback)
assertFalse(redirect.hasMarketplaceIntent())
- assertTrue(redirect.hasFallback())
+ assertFalse(redirect.hasFallback())
}
@Test
@@ -165,7 +165,7 @@ class AppLinksUseCasesTest {
val redirect = subject.interceptedAppLinkRedirect(appIntentWithPackageAndFallback)
assertFalse(redirect.hasExternalApp())
assertTrue(redirect.hasMarketplaceIntent())
- assertTrue(redirect.hasFallback())
+ assertFalse(redirect.hasFallback())
}
@Test
@@ -348,9 +348,7 @@ class AppLinksUseCasesTest {
val redirect = subject.interceptedAppLinkRedirect(uri)
assertFalse(redirect.hasExternalApp())
- assertTrue(redirect.hasFallback())
-
- assertEquals("http://zxing.org", redirect.fallbackUrl)
+ assertFalse(redirect.hasFallback())
}
@Test
@@ -557,27 +555,6 @@ class AppLinksUseCasesTest {
assertTrue(redirect.appIntent?.flags?.and(Intent.FLAG_ACTIVITY_CLEAR_TASK) == 0)
}
- @Test
- fun `WHEN opening a app scheme uri WITH fallback URL THEN use fallback if needed`() {
- val context = createContext(Triple(appIntentWithPackageAndFallback, appPackage, ""))
-
- var subject = AppLinksUseCases(context, { false })
- var redirect = subject.interceptedAppLinkRedirect(appIntentWithPackageAndFallback)
- assertFalse(redirect.hasExternalApp())
- assertTrue(redirect.hasFallback())
- assertTrue(redirect.marketplaceIntent != null)
- assertEquals(redirect.fallbackUrl, "https://example.com")
-
- AppLinksUseCases.clearRedirectCache()
- subject = AppLinksUseCases(context, { true })
- redirect = subject.interceptedAppLinkRedirect(appIntentWithPackageAndFallback)
- assertTrue(redirect.hasExternalApp())
- assertTrue(redirect.hasFallback())
- assertTrue(redirect.marketplaceIntent != null)
- assertEquals(redirect.fallbackUrl, "https://example.com")
- assertTrue(redirect.appIntent?.flags?.and(Intent.FLAG_ACTIVITY_CLEAR_TASK) == 0)
- }
-
@Test
fun `WHEN opening a app scheme uri THEN tries to redirect`() {
val context = createContext(Triple(appIntent, appPackage, ""))
@@ -695,27 +672,26 @@ class AppLinksUseCasesTest {
val subject = AppLinksUseCases(context, { false })
val redirect = subject.interceptedAppLinkRedirect(appIntentWithPackageAndPlayStoreFallback)
assertFalse(redirect.hasExternalApp())
- assertTrue(redirect.hasFallback())
- assertEquals("https://play.google.com/store/abc", redirect.fallbackUrl)
+ assertFalse(redirect.hasFallback())
}
@Test
- fun `WHEN A intent WITH android fallback link THEN fallback should be used`() {
+ fun `WHEN A intent WITH android fallback link THEN fallback should NOT be used`() {
val context = createContext()
val subject = AppLinksUseCases(context, { true })
val redirect = subject.interceptedAppLinkRedirect(urlWithAndroidFallbackLink)
- assertNotNull(redirect.fallbackUrl)
- assertTrue(redirect.hasFallback())
+ assertNull(redirect.fallbackUrl)
+ assertFalse(redirect.hasFallback())
}
@Test
- fun `WHEN A intent WITH fallback link THEN fallback should be used`() {
+ fun `WHEN A intent WITH fallback link THEN fallback should NOT be used`() {
val context = createContext()
val subject = AppLinksUseCases(context, { true })
val redirect = subject.interceptedAppLinkRedirect(urlWithFallbackLink)
- assertNotNull(redirect.fallbackUrl)
- assertTrue(redirect.hasFallback())
+ assertNull(redirect.fallbackUrl)
+ assertFalse(redirect.hasFallback())
}
}
Regression Test / PoC
diff --git a/mobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksUseCasesTest.kt b/mobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksUseCasesTest.kt
index 0a963b27b8a..b0b37af15da 100644
--- a/mobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksUseCasesTest.kt
+++ b/mobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksUseCasesTest.kt
@@ -154,7 +154,7 @@ class AppLinksUseCasesTest {
val redirect = subject.interceptedAppLinkRedirect(appIntentWithPackageAndFallback)
assertFalse(redirect.hasMarketplaceIntent())
- assertTrue(redirect.hasFallback())
+ assertFalse(redirect.hasFallback())
}
@Test
@@ -165,7 +165,7 @@ class AppLinksUseCasesTest {
val redirect = subject.interceptedAppLinkRedirect(appIntentWithPackageAndFallback)
assertFalse(redirect.hasExternalApp())
assertTrue(redirect.hasMarketplaceIntent())
- assertTrue(redirect.hasFallback())
+ assertFalse(redirect.hasFallback())
}
@Test
@@ -348,9 +348,7 @@ class AppLinksUseCasesTest {
val redirect = subject.interceptedAppLinkRedirect(uri)
assertFalse(redirect.hasExternalApp())
- assertTrue(redirect.hasFallback())
-
- assertEquals("http://zxing.org", redirect.fallbackUrl)
+ assertFalse(redirect.hasFallback())
}
@Test
@@ -557,27 +555,6 @@ class AppLinksUseCasesTest {
assertTrue(redirect.appIntent?.flags?.and(Intent.FLAG_ACTIVITY_CLEAR_TASK) == 0)
}
- @Test
- fun `WHEN opening a app scheme uri WITH fallback URL THEN use fallback if needed`() {
- val context = createContext(Triple(appIntentWithPackageAndFallback, appPackage, ""))
-
- var subject = AppLinksUseCases(context, { false })
- var redirect = subject.interceptedAppLinkRedirect(appIntentWithPackageAndFallback)
- assertFalse(redirect.hasExternalApp())
- assertTrue(redirect.hasFallback())
- assertTrue(redirect.marketplaceIntent != null)
- assertEquals(redirect.fallbackUrl, "https://example.com")
-
- AppLinksUseCases.clearRedirectCache()
- subject = AppLinksUseCases(context, { true })
- redirect = subject.interceptedAppLinkRedirect(appIntentWithPackageAndFallback)
- assertTrue(redirect.hasExternalApp())
- assertTrue(redirect.hasFallback())
- assertTrue(redirect.marketplaceIntent != null)
- assertEquals(redirect.fallbackUrl, "https://example.com")
- assertTrue(redirect.appIntent?.flags?.and(Intent.FLAG_ACTIVITY_CLEAR_TASK) == 0)
- }
-
@Test
fun `WHEN opening a app scheme uri THEN tries to redirect`() {
val context = createContext(Triple(appIntent, appPackage, ""))
@@ -695,27 +672,26 @@ class AppLinksUseCasesTest {
val subject = AppLinksUseCases(context, { false })
val redirect = subject.interceptedAppLinkRedirect(appIntentWithPackageAndPlayStoreFallback)
assertFalse(redirect.hasExternalApp())
- assertTrue(redirect.hasFallback())
- assertEquals("https://play.google.com/store/abc", redirect.fallbackUrl)
+ assertFalse(redirect.hasFallback())
}
@Test
- fun `WHEN A intent WITH android fallback link THEN fallback should be used`() {
+ fun `WHEN A intent WITH android fallback link THEN fallback should NOT be used`() {
val context = createContext()
val subject = AppLinksUseCases(context, { true })
val redirect = subject.interceptedAppLinkRedirect(urlWithAndroidFallbackLink)
- assertNotNull(redirect.fallbackUrl)
- assertTrue(redirect.hasFallback())
+ assertNull(redirect.fallbackUrl)
+ assertFalse(redirect.hasFallback())
}
@Test
- fun `WHEN A intent WITH fallback link THEN fallback should be used`() {
+ fun `WHEN A intent WITH fallback link THEN fallback should NOT be used`() {
val context = createContext()
val subject = AppLinksUseCases(context, { true })
val redirect = subject.interceptedAppLinkRedirect(urlWithFallbackLink)
- assertNotNull(redirect.fallbackUrl)
- assertTrue(redirect.hasFallback())
+ assertNull(redirect.fallbackUrl)
+ assertFalse(redirect.hasFallback())
}
}