Low firefox Logic Error 🔧 Commit mapped

Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactlow
DescriptionSpoofing issue in the Downloads component in Firefox for Android
ComponentCore
Bug ClassLogic Error
Tracker1842361
Fix commite90ea19c49fc (firefox) +77/-0
CISA KEVNot listed
CreditedHafiizh
Disclosed2026-08-18

Files Changed

  • mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadsFeature.kt
  • mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadsFeatureTest.kt
  • mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/AddonPopupBaseFragment.kt
  • mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadsFeature.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadsFeature.kt
index 896701bbd5c..eefbf26fb7e 100644
--- a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadsFeature.kt
+++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadsFeature.kt
@@ -123,6 +123,8 @@ value class OpenFileCallback(val value: () -> Unit)
  * manager is provided, a dialog will be shown before every download.
  * @property promptsStyling styling properties for the dialog.
  * @property onDownloadStartedListener a callback invoked when a download is started.
+ * @property dismissCustomFirstPartyDownloadDialog A callback invoked when the custom first party
+ * download dialog should be dismissed.
  * @property shouldForwardToThirdParties Indicates if downloads should be forward to third party apps,
  * if there are multiple apps a chooser dialog will shown.
  * @property customFirstPartyDownloadDialog An optional delegate for showing a dialog for a download
@@ -146,6 +148,7 @@ class DownloadsFeature(
     private val fragmentManager: FragmentManager? = null,
     private val promptsStyling: PromptsStyling? = null,
     private val onDownloadStartedListener: ((String) -> Unit) = {},
+    private val dismissCustomFirstPartyDownloadDialog: () -> Unit = {},
     private val shouldForwardToThirdParties: () -> Boolean = { false },
     private val customFirstPartyDownloadDialog: (
         (
@@ -556,6 +559,7 @@ class DownloadsFeature(
     internal fun dismissAllDownloadDialogs() {
         findPreviousDownloadDialogFragment()?.dismiss()
         findPreviousAppDownloaderDialogFragment()?.dismiss()
+        dismissCustomFirstPartyDownloadDialog.invoke()
     }
 
     private val ActivityInfo.identifier: String get() = packageName + name
diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadsFeatureTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadsFeatureTest.kt
index ee4f52f8e7a..5dc52d87826 100644
--- a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadsFeatureTest.kt
+++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadsFeatureTest.kt
@@ -1460,6 +1460,64 @@ class DownloadsFeatureTest {
         verify(cancelDownloadRequestUseCase).invoke(anyString(), anyString())
     }
 
+    @Test
+    fun `GIVEN a custom download dialog is used WHEN dismissAllDownloadDialogs is called THEN the dialog is dismissed`() = runTest(testDispatcher) {
+        val dismissCustomDialog = mock<() -> Unit>()
+        val feature = DownloadsFeature(
+            testContext,
+            store,
+            useCases = DownloadsUseCases(store, mock()),
+            downloadFileUtils = FakeDownloadFileUtils(),
+            downloadManager = mock(),
+            mainDispatcher = testDispatcher,
+            dismissCustomFirstPartyDownloadDialog = dismissCustomDialog,
+        )
+
+        feature.dismissAllDownloadDialogs()
+
+        verify(dismissCustomDialog).invoke()
+    }
+
+    @Test
+    fun `GIVEN a custom download dialog is used WHEN navigating to another website THEN the dialog is dismissed`() = runTest(testDispatcher) {
+        val dismissCustomDialog = mock<() -> Unit>()
+        val downloadsUseCases = spy(DownloadsUseCases(store, mock()))
+        val cancelDownloadRequestUseCase = mock<CancelDownloadRequestUseCase>()
+        val download = DownloadState(url = "https://www.mozilla.org", sessionId = "test-tab")
+        store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download = download))
+
+        doReturn(cancelDownloadRequestUseCase).`when`(downloadsUseCases).cancelDownloadRequest
+
+        val feature = spy(
+            DownloadsFeature(
+                testContext,
+                store,
+                useCases = downloadsUseCases,
+                downloadFileUtils = FakeDownloadFileUtils(),
+                downloadManager = mock(),
+                mainDispatcher = testDispatcher,
+                dismissCustomFirstPartyDownloadDialog = dismissCustomDialog,
+            ),
+        )
+
+        doReturn(true).`when`(feature).processDownload(any(), any())
+
+        feature.start()
+        testDispatcher.scheduler.advanceUntilIdle()
+
+        store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download = download))
+        testDispatcher.scheduler.advanceUntilIdle()
+
+        grantPermissions()
+
+        val tab = createTab("https://www.firefox.com")
+        store.dispatch(TabListAction.AddTabAction(tab, select = true))
+        testDispatcher.scheduler.advanceUntilIdle()
+
+        verify(feature).dismissAllDownloadDialogs()
+        verify(dismissCustomDialog).invoke()
+    }
+
     @Test
     fun `ResolveInfo to DownloaderApps`() = runTest(testDispatcher) {
         val spyContext = spy(testContext)
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/AddonPopupBaseFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/AddonPopupBaseFragment.kt
index 2b99f6a892a..08200d7052e 100644
--- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/AddonPopupBaseFragment.kt
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/AddonPopupBaseFragment.kt
@@ -161,6 +161,10 @@ abstract class AddonPopupBaseFragment :
                 onNeedToRequestPermissions = { permissions ->
                     requestPermissions(permissions, REQUEST_CODE_DOWNLOAD_PERMISSIONS)
                 },
+                dismissCustomFirstPartyDownloadDialog = {
+                    dismissRenameDialog()
+                    downloadDialog?.dismiss()
+                },
                 customFirstPartyDownloadDialog = { currentDownloadState, _, positiveAction, negativeAction, _ ->
                     run {
                         if (canShowDownloadDialog()) {
@@ -394,6 +398,13 @@ abstract class AddonPopupBaseFragment :
         return downloadDialog == null && !isRenameFragmentShowing
     }
 
+    private fun dismissRenameDialog() {
+        val renameDialog = childFragmentManager.findFragmentByTag(
+            RenameAndChangeLocationDialogFragment.RENAME_AND_CHANGE_LOCATION_DIALOG_TAG,
+        ) as? RenameAndChangeLocationDialogFragment
+        renameDialog?.dismissAllowingStateLoss()
+    }
+
     /**
      * Forwards activity results to the [ActivityResultHandler] features.
      */
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt
index 314b0b195a5..b2eeea2f8b5 100644
--- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt
@@ -742,6 +742,10 @@ abstract class BaseBrowserFragment :
             onNeedToRequestPermissions = { permissions ->
                 requestPermissions(permissions, REQUEST_CODE_DOWNLOAD_PERMISSIONS)
             },
+            dismissCustomFirstPartyDownloadDialog = {
+                dismissRenameDialog()
+                dismissDownloadDialogs()
+            },
             customFirstPartyDownloadDialog = {
                     currentDownloadState,
                     fileNameIfAlreadyDownloaded,
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadsFeatureTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadsFeatureTest.kt
index ee4f52f8e7a..5dc52d87826 100644
--- a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadsFeatureTest.kt
+++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadsFeatureTest.kt
@@ -1460,6 +1460,64 @@ class DownloadsFeatureTest {
         verify(cancelDownloadRequestUseCase).invoke(anyString(), anyString())
     }
 
+    @Test
+    fun `GIVEN a custom download dialog is used WHEN dismissAllDownloadDialogs is called THEN the dialog is dismissed`() = runTest(testDispatcher) {
+        val dismissCustomDialog = mock<() -> Unit>()
+        val feature = DownloadsFeature(
+            testContext,
+            store,
+            useCases = DownloadsUseCases(store, mock()),
+            downloadFileUtils = FakeDownloadFileUtils(),
+            downloadManager = mock(),
+            mainDispatcher = testDispatcher,
+            dismissCustomFirstPartyDownloadDialog = dismissCustomDialog,
+        )
+
+        feature.dismissAllDownloadDialogs()
+
+        verify(dismissCustomDialog).invoke()
+    }
+
+    @Test
+    fun `GIVEN a custom download dialog is used WHEN navigating to another website THEN the dialog is dismissed`() = runTest(testDispatcher) {
+        val dismissCustomDialog = mock<() -> Unit>()
+        val downloadsUseCases = spy(DownloadsUseCases(store, mock()))
+        val cancelDownloadRequestUseCase = mock<CancelDownloadRequestUseCase>()
+        val download = DownloadState(url = "https://www.mozilla.org", sessionId = "test-tab")
+        store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download = download))
+
+        doReturn(cancelDownloadRequestUseCase).`when`(downloadsUseCases).cancelDownloadRequest
+
+        val feature = spy(
+            DownloadsFeature(
+                testContext,
+                store,
+                useCases = downloadsUseCases,
+                downloadFileUtils = FakeDownloadFileUtils(),
+                downloadManager = mock(),
+                mainDispatcher = testDispatcher,
+                dismissCustomFirstPartyDownloadDialog = dismissCustomDialog,
+            ),
+        )
+
+        doReturn(true).`when`(feature).processDownload(any(), any())
+
+        feature.start()
+        testDispatcher.scheduler.advanceUntilIdle()
+
+        store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download = download))
+        testDispatcher.scheduler.advanceUntilIdle()
+
+        grantPermissions()
+
+        val tab = createTab("https://www.firefox.com")
+        store.dispatch(TabListAction.AddTabAction(tab, select = true))
+        testDispatcher.scheduler.advanceUntilIdle()
+
+        verify(feature).dismissAllDownloadDialogs()
+        verify(dismissCustomDialog).invoke()
+    }
+
     @Test
     fun `ResolveInfo to DownloaderApps`() = runTest(testDispatcher) {
         val spyContext = spy(testContext)
Loading diff…