Medium firefox Logic Error 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionClickjacking issue in the WebExtensions component in Firefox for Android
ComponentCore
Bug ClassLogic Error
Tracker2047608
Fix commit70c8f6cb58b3 (firefox) +29/-6
CISA KEVNot listed
CreditedHafiizh
Disclosed2026-07-21

Background

Clickjacking
a UI-redress attack where a malicious overlay tricks a user into interacting with a security-relevant control they cannot see.
`filterTouchesWhenObscured`
an Android View flag that makes the framework discard touch events delivered while another window is drawn on top of the view.
`DialogFragment`
the AndroidX base class used by Firefox for Android to host modal UI such as add-on, prompt, and site-permission dialogs.
WebExtensions add-on dialogs
Fenix/GeckoView dialogs (install, download, permission grant) that confirm privileged WebExtension actions on the user’s behalf.

Root Cause Analysis

The security-relevant dialogs in Firefox for Android (AddonDialogFragment, PromptDialogFragment, SitePermissionsDialogFragment, and Fenix’s DownloadAddonDialogFragment) all extended AppCompatDialogFragment directly and never set filterTouchesWhenObscured on their window’s decorView. Because of this, the dialogs’ consent buttons continued to accept touch events even when another window (for example a malicious overlay app or a SYSTEM_ALERT_WINDOW view) was drawn on top of them, violating the invariant that a user’s tap on a security decision must land on a control the user can actually see. An attacker-controlled overlay could therefore visually hide the real dialog while a user’s tap passed through to the underlying “allow” or “install” control.

The fix introduces a shared NoObscuredTouchesDialogFragment base class whose onStart() sets filterTouchesWhenObscured = true on the dialog window’s decorView, and re-parents each affected dialog onto it. With the flag set, Android drops any touch that is delivered while the dialog is obscured, so an overlay can no longer smuggle taps into the consent controls.

Key insight
The single mistake was that consent dialogs trusted every touch event without checking whether their window was visually obscured; the fix centralizes the filterTouchesWhenObscured protection in a common base class so obscured touches are discarded rather than acted upon.

Attack Path

  1. Install an overlay app A malicious Android app obtains overlay drawing capability (e.g. SYSTEM_ALERT_WINDOW) and monitors for Firefox activity.
  2. Trigger a security dialog A web page or extension causes Firefox to display an add-on, prompt, or site-permission dialog with a consent button.
  3. Draw a deceptive overlay The app renders a benign-looking UI on top of the dialog, hiding the real consent prompt while leaving the button’s touch target active.
  4. Capture the user's tap The user taps what they believe is the overlay’s control, and because touches are not filtered the tap passes through to the hidden “allow”/“install” button.
  5. Grant the privileged action Firefox treats the tap as genuine consent and proceeds to install the add-on or grant the site permission.

Impact Assessment

An attacker with an overlay-capable app on the device can induce the user to unknowingly approve privileged WebExtension or site-permission actions in Firefox for Android, gaining consent they never intended to give. This operates entirely on the Android application/UI layer of the browser and requires a malicious app already present on the device with overlay privileges plus a user interacting with a Firefox dialog. Severity is moderate because it depends on local malware and user interaction rather than remote code execution.

Changed Functions

FunctionChangeNotes
DownloadAddonDialogFragment
mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/DownloadAddonDialogFragment.kt
modified

Files Changed

  • mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/AddonDialogFragment.kt
  • mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/PromptDialogFragment.kt
  • mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/SitePermissionsDialogFragment.kt
  • mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/android/NoObscuredTouchesDialogFragment.kt
  • mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/DownloadAddonDialogFragment.kt

Audit Directions

  • Obscured-touch protection on consent UI
    Audit every DialogFragment, Dialog, and custom View that gates a security or privacy decision to confirm it sets filterTouchesWhenObscured (or extends NoObscuredTouchesDialogFragment).
  • New dialogs bypassing the base class
    Flag any new or existing security-relevant dialog that still extends AppCompatDialogFragment or DialogFragment directly instead of the hardened base class.
  • Overlay-sensitive controls
    Review buttons that trigger installs, permission grants, or credential entry for tapjacking resistance, since these are the highest-value targets for overlay attacks on Android.
diff --git a/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/AddonDialogFragment.kt b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/AddonDialogFragment.kt
index 786153f4084..fef65fd18f8 100644
--- a/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/AddonDialogFragment.kt
+++ b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/AddonDialogFragment.kt
@@ -12,6 +12,7 @@ import androidx.appcompat.app.AppCompatDialogFragment
 import androidx.appcompat.widget.AppCompatImageView
 import androidx.core.graphics.drawable.toDrawable
 import mozilla.components.feature.addons.Addon
+import mozilla.components.support.base.android.NoObscuredTouchesDialogFragment
 import mozilla.components.support.utils.ext.getParcelableCompat
 
 @VisibleForTesting
@@ -20,7 +21,7 @@ internal const val KEY_ICON = "KEY_ICON"
 /**
  * A generic [Addon] dialog which has an [Addon]'s icon.
  */
-open class AddonDialogFragment : AppCompatDialogFragment() {
+open class AddonDialogFragment : NoObscuredTouchesDialogFragment() {
     init {
         arguments = arguments ?: Bundle()
     }
diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/PromptDialogFragment.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/PromptDialogFragment.kt
index 093c389cbfe..8cce0660fe8 100644
--- a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/PromptDialogFragment.kt
+++ b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/PromptDialogFragment.kt
@@ -4,11 +4,11 @@
 
 package mozilla.components.feature.prompts.dialog
 
-import androidx.fragment.app.DialogFragment
 import mozilla.components.concept.engine.prompt.PromptRequest
 import mozilla.components.concept.storage.CreditCardValidationDelegate
 import mozilla.components.concept.storage.LoginValidationDelegate
 import mozilla.components.feature.prompts.login.LoginExceptions
+import mozilla.components.support.base.android.NoObscuredTouchesDialogFragment
 
 internal const val KEY_SESSION_ID = "KEY_SESSION_ID"
 internal const val KEY_TITLE = "KEY_TITLE"
@@ -20,7 +20,7 @@ internal const val KEY_SHOULD_DISMISS_ON_LOAD = "KEY_SHOULD_DISMISS_ON_LOAD"
  * An abstract representation for all different types of prompt dialogs.
  * for handling [PromptFeature] dialogs.
  */
-internal abstract class PromptDialogFragment : DialogFragment() {
+internal abstract class PromptDialogFragment : NoObscuredTouchesDialogFragment() {
     var feature: Prompter? = null
 
     internal val sessionId: String by lazy { requireNotNull(arguments).getString(KEY_SESSION_ID)!! }
diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/SitePermissionsDialogFragment.kt b/mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/SitePermissionsDialogFragment.kt
index 3d9b44f1208..8a9fdbc2b75 100644
--- a/mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/SitePermissionsDialogFragment.kt
+++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/SitePermissionsDialogFragment.kt
@@ -23,11 +23,11 @@ import android.widget.ImageView
 import android.widget.LinearLayout.LayoutParams
 import android.widget.TextView
 import androidx.annotation.VisibleForTesting
-import androidx.appcompat.app.AppCompatDialogFragment
 import androidx.appcompat.content.res.AppCompatResources
 import androidx.core.app.NotificationManagerCompat
 import androidx.core.content.ContextCompat
 import androidx.core.graphics.drawable.toDrawable
+import mozilla.components.support.base.android.NoObscuredTouchesDialogFragment
 import mozilla.components.support.base.log.logger.Logger
 import mozilla.components.support.ktx.android.content.appName
 import mozilla.components.support.ktx.kotlin.ifNullOrEmpty
@@ -51,7 +51,7 @@ private const val KEY_IS_NOTIFICATION_REQUEST = "KEY_IS_NOTIFICATION_REQUEST"
 private const val DEFAULT_VALUE = Int.MAX_VALUE
 private const val KEY_PERMISSION_ID = "KEY_PERMISSION_ID"
 
-internal open class SitePermissionsDialogFragment : AppCompatDialogFragment() {
+internal open class SitePermissionsDialogFragment : NoObscuredTouchesDialogFragment() {
 
     private val logger = Logger("SitePermissionsDialogFragment")
 
diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/android/NoObscuredTouchesDialogFragment.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/android/NoObscuredTouchesDialogFragment.kt
new file mode 100644
index 00000000000..f4b94d81210
--- /dev/null
+++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/android/NoObscuredTouchesDialogFragment.kt
@@ -0,0 +1,21 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.support.base.android
+
+import androidx.annotation.CallSuper
+import androidx.appcompat.app.AppCompatDialogFragment
+
+/**
+ * [AppCompatDialogFragment] that responds to touches only if not obscured
+ */
+open class NoObscuredTouchesDialogFragment: AppCompatDialogFragment() {
+    @CallSuper
+    override fun onStart() {
+        super.onStart()
+        dialog?.window?.decorView?.apply {
+            filterTouchesWhenObscured = true
+        }
+    }
+}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/DownloadAddonDialogFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/DownloadAddonDialogFragment.kt
index f605eb7c5a0..4195d760d32 100644
--- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/DownloadAddonDialogFragment.kt
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/DownloadAddonDialogFragment.kt
@@ -43,6 +43,7 @@ import androidx.core.graphics.drawable.toDrawable
 import androidx.fragment.compose.content
 import androidx.navigation.fragment.navArgs
 import mozilla.components.compose.base.button.TextButton
+import mozilla.components.support.base.android.NoObscuredTouchesDialogFragment
 import org.mozilla.fenix.compose.Favicon
 import org.mozilla.fenix.theme.FirefoxTheme
 import org.mozilla.fenix.theme.PreviewThemeProvider
@@ -60,7 +61,7 @@ import mozilla.components.ui.icons.R as iconsR
  *
  * @see [DownloadAddonDialogFragmentArgs].
  */
-class DownloadAddonDialogFragment : AppCompatDialogFragment() {
+class DownloadAddonDialogFragment : NoObscuredTouchesDialogFragment() {
     private val args by navArgs<DownloadAddonDialogFragmentArgs>()
 
     /**
Loading diff…