Medium CVSS 4.3 firefox Spoofing 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionWhen a URL was provided in a link querystring parameter, Firefox for Android would follow that URL instead of the correct URL, potentially leading to phishing attacks.<br>*This bug only affects Firefox for Android. Other versions of Firefox are unaffected.*
ComponentCore
Bug ClassSpoofing
Tracker1970151
Fix commit1600b429548b (firefox) +11/-49
CWECWE-601
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N
CISA KEVNot listed
CreditedRaul Bucata
Disclosed2025-06-24

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_url is the standard Android intent extra for a fallback; afl and link were additionally read as query-string parameters of the URL itself.
`getHierarchicalUrl()`
The removed helper that derived a fallback URL by reading the afl or link query parameter of the incoming URL, falling back to the intent’s browser_fallback_url extra.

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.

Key insight
Firefox for Android derived an app-link’s fallback navigation target from attacker-controllable 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

  1. Craft link Attacker builds an app-scheme/intent: link whose query string contains link= or afl= pointing at an attacker-chosen phishing URL. (Firefox for Android only)
  2. Lure The victim taps the link, which appears associated with a legitimate app/destination. (UI:R)
  3. Interception AppLinksUseCases.interceptedAppLinkRedirect processes it; the targeted app is unavailable or declines, so a fallback is used.
  4. Follow attacker URL getHierarchicalUrl() reads the link/afl query parameter and sets it as fallbackUrl, so Firefox navigates there instead of the correct URL.
  5. Primitive Open redirect to an attacker-controlled page, enabling spoofing/phishing against the user.

Impact Assessment

Affects only Firefox for Android. No privilege is required; the attacker only needs the user to tap a crafted link (UI:R), and the impact is a low-integrity spoofing/open-redirect primitive (CVSS 4.3, integrity-low) — the browser is steered to an attacker URL, facilitating phishing. There is no memory-safety consequence or code execution; the harm is user deception.

Files Changed

  • mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/AppLinksUseCases.kt
  • mobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksUseCasesTest.kt

Audit Directions

  • Other intent-extra consumers
    Review remaining use of EXTRA_BROWSER_FALLBACK_URL and any code that still reads URLs out of intent extras/query parameters for navigation targets.
  • Redirect/navigation from untrusted parameters
    Audit other Android navigation paths (custom tabs, deep links) that build a destination from query-string values.
  • Marketplace/Play-Store fallback
    Confirm the removal did not reintroduce a different attacker-influenced fallback in the marketplace-intent branch.
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())
     }
 }
Loading diff…

Regression Test / PoC

shipped with the fix
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())
     }
 }
Loading diff…