CVE-2026-16397
Overview
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
Viewflag 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.
filterTouchesWhenObscured protection in a common base class so obscured touches are discarded rather than acted upon.Attack Path
- Install an overlay app
A malicious Android app obtains overlay drawing capability (e.g.
SYSTEM_ALERT_WINDOW) and monitors for Firefox activity. - 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.
- 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.
- 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.
- 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
Changed Functions
| Function | Change | Notes |
|---|---|---|
DownloadAddonDialogFragmentmobile/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.ktmobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/PromptDialogFragment.ktmobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/SitePermissionsDialogFragment.ktmobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/android/NoObscuredTouchesDialogFragment.ktmobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/DownloadAddonDialogFragment.kt
Audit Directions
- Obscured-touch protection on consent UIAudit every
DialogFragment,Dialog, and customViewthat gates a security or privacy decision to confirm it setsfilterTouchesWhenObscured(or extendsNoObscuredTouchesDialogFragment). - New dialogs bypassing the base classFlag any new or existing security-relevant dialog that still extends
AppCompatDialogFragmentorDialogFragmentdirectly instead of the hardened base class. - Overlay-sensitive controlsReview buttons that trigger installs, permission grants, or credential entry for tapjacking resistance, since these are the highest-value targets for overlay attacks on Android.
Patch
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>()
/**