CVE-2024-11701
Overview
Background
- `GeckoEngineSession`
- the Android Components wrapper around a Gecko rendering session that bridges Gecko delegate callbacks into the app’s observer/store model.
- `onLocationChange`
- the observer notification that updates the URL the app considers current (and thus what the address bar displays) for a session.
- `onPageStart`
- the Gecko
ProgressDelegatecallback fired when a page load begins, where progress and loading state are propagated to observers. - Interrupted navigation
- a load that begins for a target URL but is cancelled or redirected (for example a scheme link that opens an external-app prompt), leaving the display state to be reconciled.
Root Cause Analysis
When a navigation started in GeckoEngineSession, the onPageStart callback notified observers with onProgress(PROGRESS_START) and onLoadingStateChange(true) but did not immediately notify them of the destination URL via onLocationChange. The invariant that the store’s displayed URL must always correspond to the load actually in progress was therefore violated during the window between the start of a load and the eventual (or never-arriving) location update. If the navigation was interrupted — such as a link that triggered an external-app prompt that the user then cancelled — the address bar could be left showing the previous or intended origin rather than the URL truly reflecting session state, enabling a mismatch between displayed and effective context.
The fix adds onLocationChange(url, hasUserGesture = false) inside the same onPageStart notification block so the store’s URL is updated synchronously at load start, keeping the address bar consistent even when the load is aborted.
onPageStart announced that a new load had begun without announcing which URL it was for; the fix eliminates that gap by emitting onLocationChange in the same notification block as onLoadingStateChange.Attack Path
- Craft a spoofing target
An attacker hosts a page containing a link to a scheme or resource that will trigger an interruptible navigation (for example a
vnd.youtube://style external-app link) from an origin the victim trusts visually. - Victim initiates the navigation
The victim taps the link,
onPageStartfires and the loading state flips, but noonLocationChangeis emitted, so the displayed URL does not yet reflect the real load target. - Navigation is interrupted The load is diverted to an external-app prompt (or otherwise cancelled), and the victim declines it, so no committed location update ever arrives to correct the address bar.
- Stale address bar persists The address bar continues to display an incorrect domain that does not match the session’s actual state, giving the victim a false sense of which site they are viewing.
- Deception is leveraged The attacker exploits the mismatched address bar to make content appear to originate from a trusted domain, supporting a phishing or spoofing attack.
Impact Assessment
Files Changed
mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngineSession.ktmobile/android/android-components/docs/changelog.mdmobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAdvancedTest.kt
Audit Directions
- Deferred UI-state updatesLook for callbacks that announce a state transition (loading started, progress) without synchronously announcing the associated identity (URL/origin), leaving a reconciliation gap.
- Interrupted/cancelled navigation pathsAudit code paths where a navigation can be aborted, redirected, or handed to an external handler, and verify the address bar and session URL are corrected on every abort branch.
- Observer notification orderingReview
notifyObserversblocks and delegate bridges for cases whereonLoadingStateChangeor progress events are emitted without a pairedonLocationChange, which can desynchronize displayed and actual state.
Patch
diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngineSession.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngineSession.kt
index ddfe9b33c2b..e77f1f79cf2 100644
--- a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngineSession.kt
+++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngineSession.kt
@@ -1329,6 +1329,7 @@ class GeckoEngineSession(
notifyObservers {
onProgress(PROGRESS_START)
onLoadingStateChange(true)
+ onLocationChange(url, hasUserGesture = false)
}
}
diff --git a/mobile/android/android-components/docs/changelog.md b/mobile/android/android-components/docs/changelog.md
index b396b012b5a..05edc9a41ff 100644
--- a/mobile/android/android-components/docs/changelog.md
+++ b/mobile/android/android-components/docs/changelog.md
@@ -215,6 +215,9 @@ permalink: /changelog/
* **feature-customtabs**
* Fallback behaviour when failing to open a new window in custom tab will now be loading the URL directly in the same custom tab. [Bug 1832357](https://bugzilla.mozilla.org/show_bug.cgi?id=1832357)
+* **feature-session**
+ * Update URL in the store immediately during onPageStart.
+
* **tooling-lint**
* Added a lint rule to detect when `Response#close` may not have been called. Note: Currently, this rule only runs on Android Components.
diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAdvancedTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAdvancedTest.kt
index 93aa24ce80f..d19425c5a62 100644
--- a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAdvancedTest.kt
+++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAdvancedTest.kt
@@ -31,6 +31,7 @@ class SettingsAdvancedTest : TestSetup() {
private val youTubeSchemaLink = itemContainingText("Youtube schema link")
private val playStoreLink = itemContainingText("Playstore link")
private val playStoreUrl = "play.google.com"
+ private val youtubeUrl = "vnd.youtube://Mozilla"
@get:Rule
val activityIntentTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides()
@@ -138,7 +139,7 @@ class SettingsAdvancedTest : TestSetup() {
clickPageObject(youTubeSchemaLink)
verifyOpenLinkInAnotherAppPrompt()
clickPageObject(itemWithResIdAndText("android:id/button2", "CANCEL"))
- verifyUrl(externalLinksPage.url.toString())
+ verifyUrl(youtubeUrl)
}
}
@@ -207,7 +208,7 @@ class SettingsAdvancedTest : TestSetup() {
pageObject = youTubeSchemaLink,
)
clickPageObject(itemWithResIdAndText("android:id/button2", "CANCEL"))
- verifyUrl(externalLinksPage.url.toString())
+ verifyUrl(youtubeUrl)
}
}