Medium firefox Info Disclosure 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionInformation disclosure in the Privacy component in Firefox for Android
ComponentCore
Bug ClassInfo Disclosure
Tracker2021964
Fix commita0dd12d38cbd (firefox) +88/-46
CISA KEVNot listed
CreditedSatoki Tsuji
Disclosed2026-07-21

Background

Private browsing tab
a Firefox for Android session whose browsing state, including the URL, is meant not to persist or leak outside the session.
`ClipboardHandler`
a mozilla-components helper wrapping the Android clipboard, exposing both text and a sensitiveText setter that flags the copied data.
`ClipDescription.EXTRA_IS_SENSITIVE`
an Android 13+ (TIRAMISU) clipboard flag that tells the OS to redact the copied value from the system paste preview and other observers.
`isPrivate` flag
the SessionState.content.private boolean on the selected or custom tab that indicates whether the current page belongs to a private session.

Root Cause Analysis

Every clipboard-copy path for the current page URL — the main toolbar (BrowserToolbarMiddleware), custom tabs (CustomTabBrowserToolbarMiddleware), the long-press toolbar popup (ToolbarPopupWindow), and the share sheet’s copy action (DefaultShareController) — wrote the URL with the plain clipboard.text setter (or a bare ClipData with no extras) regardless of whether the tab was private. Because the copied ClipData carried no EXTRA_IS_SENSITIVE marker, Android 13+ treated a private-tab URL like any other clipboard entry, displaying it in the system copy-confirmation popup and exposing it to any app that reads the clipboard. The invariant violated is that data originating from a private session must be marked sensitive so the platform does not render or share it in the clear.

The fix reads the tab’s private state at each copy site and routes private URLs through clipboard.sensitiveText (or sets EXTRA_IS_SENSITIVE on the ClipDescription in the share controller), which stamps the platform flag. The custom-tab and popup paths additionally fail safe, defaulting to sensitiveText whenever the private state is null or not explicitly false.

Key insight
The single mistake was copying private-browsing URLs to the clipboard without ever propagating the tab’s private state into a sensitivity flag, so the OS could not redact them; the fix conditions every copy path on content.private and uses sensitiveText / EXTRA_IS_SENSITIVE for private tabs, defaulting to sensitive when the state is unknown.

Attack Path

  1. Browse privately The victim opens a URL in a private tab that they expect to remain confidential.
  2. Copy the URL The victim copies the address via the toolbar, a custom tab, the long-press popup, or the share sheet’s copy option, all of which stored it as non-sensitive.
  3. System exposure On Android 13+ the OS renders the un-flagged URL in the copy-confirmation preview popup, revealing it on screen.
  4. Clipboard observation Any other installed app able to read the primary clipboard retrieves the private URL because it was never marked sensitive.

Impact Assessment

An attacker or a snooping app gains disclosure of a URL from a private browsing session — content the user intended to keep confidential — on Firefox for Android running Android 13 or newer. Exploitation requires that the user copy a private-tab URL to the clipboard via one of the affected UI paths; there is no need for the attacker to control the browser process itself. The exposure is limited to information the user copies, not arbitrary browser state.

Changed Functions

FunctionChangeNotes
if
mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarMiddleware.kt
modified
if
mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/CustomTabBrowserToolbarMiddleware.kt
modified
if
mobile/android/fenix/app/src/main/java/org/mozilla/fenix/share/ShareController.kt
modified
if
mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/ToolbarPopupWindow.kt
modified

Files Changed

  • mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarMiddleware.kt
  • mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/CustomTabBrowserToolbarMiddleware.kt
  • mobile/android/fenix/app/src/main/java/org/mozilla/fenix/share/ShareController.kt
  • mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/ToolbarPopupWindow.kt

Audit Directions

  • Clipboard writes of session data
    grep for clipboard.text, setPrimaryClip, and ClipData.newPlainText and confirm each site marks private-session content sensitive via sensitiveText or EXTRA_IS_SENSITIVE.
  • Fail-safe default on private state
    verify that any check of content.private treats null or unknown as private (defaulting to sensitive) rather than assuming a page is non-private.
  • Private-state propagation to platform sinks
    audit other exit points (share intents, notifications, autofill, logging) where a private-tab URL or title could reach the OS or another app without a sensitivity flag.
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarMiddleware.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarMiddleware.kt
index 32c6d2ebbce..19f8ccbe027 100644
--- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarMiddleware.kt
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarMiddleware.kt
@@ -380,7 +380,13 @@ class BrowserToolbarMiddleware(
 
                 val selectedTab = browserStore.state.selectedTab
                 val url = selectedTab?.readerState?.activeUrl ?: selectedTab?.content?.url
-                clipboard.text = url
+                val isPrivate = selectedTab?.content?.private ?: true
+
+                if (isPrivate) {
+                    clipboard.sensitiveText = url
+                } else {
+                    clipboard.text = url
+                }
 
                 // Android 13+ shows by default a popup for copied text.
                 // Avoid overlapping popups informing the user when the URL is copied to the clipboard.
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/CustomTabBrowserToolbarMiddleware.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/CustomTabBrowserToolbarMiddleware.kt
index a9cded0dd92..9e3c25fcf9b 100644
--- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/CustomTabBrowserToolbarMiddleware.kt
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/CustomTabBrowserToolbarMiddleware.kt
@@ -279,24 +279,34 @@ class CustomTabBrowserToolbarMiddleware(
                 )
             }
 
-            is CopyToClipboardClicked -> {
-                Events.copyUrlTapped.record(NoExtras())
-
-                clipboard.text = customTab?.content?.url?.also {
-                    // Android 13+ shows by default a popup for copied text.
-                    // Avoid overlapping popups informing the user when the URL is copied to the clipboard.
-                    // and only show our snackbar when Android will not show an indication by default.
-                    // See https://developer.android.com/develop/ui/views/touch-and-input/copy-paste#duplicate-notifications).
-                    if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
-                        appStore.dispatch(URLCopiedToClipboard)
-                    }
-                }
-            }
+            is CopyToClipboardClicked -> handleCopyToClipboard()
 
             else -> next(action)
         }
     }
 
+    private fun handleCopyToClipboard() {
+        Events.copyUrlTapped.record(NoExtras())
+        val currentTab = customTab
+        val url = currentTab?.content?.url
+        // For added safety unless the current tab is explicitly set to non-private,
+        // fall back to sensitiveText even if the check comes back as null.
+        if (currentTab?.content?.private == false) {
+            clipboard.text = url
+        } else {
+            clipboard.sensitiveText = url
+        }
+        url?.also {
+            // Android 13+ shows by default a popup for copied text.
+            // Avoid overlapping popups informing the user when the URL is copied to the clipboard.
+            // and only show our snackbar when Android will not show an indication by default.
+            // See https://developer.android.com/develop/ui/views/touch-and-input/copy-paste#duplicate-notifications).
+            if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
+                appStore.dispatch(URLCopiedToClipboard)
+            }
+        }
+    }
+
     private fun observePageOriginUpdates(store: Store<BrowserToolbarState, BrowserToolbarAction>) {
         browserStore.observeWhileActive {
             mapNotNull { state -> state.findCustomTab(customTabId) }
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/share/ShareController.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/share/ShareController.kt
index 08537ca9377..f87689e092c 100644
--- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/share/ShareController.kt
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/share/ShareController.kt
@@ -6,6 +6,7 @@ package org.mozilla.fenix.share
 
 import android.content.ActivityNotFoundException
 import android.content.ClipData
+import android.content.ClipDescription
 import android.content.ClipboardManager
 import android.content.Context
 import android.content.Intent
@@ -16,6 +17,8 @@ import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK
 import android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT
 import android.net.Uri
 import android.os.Build
+import android.os.PersistableBundle
+import androidx.annotation.RequiresApi
 import androidx.annotation.VisibleForTesting
 import androidx.core.net.toUri
 import androidx.navigation.NavController
@@ -126,6 +129,7 @@ class DefaultShareController(
         dismiss(ShareController.Result.DISMISSED)
     }
 
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     override fun handleShareToApp(app: AppShareOption) {
         Events.shareToApp.record(
             getShareToAppSafeExtra(
@@ -296,19 +300,18 @@ class DefaultShareController(
         return "data:,${Uri.encode(this)}"
     }
 
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     private fun copyClipboard() {
         val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
         val clipData = ClipData.newPlainText(getShareSubject(), getShareText())
 
-        clipboardManager.setPrimaryClip(clipData)
-
-        // Android 13+ shows by default a popup for copied text.
-        // Avoid overlapping popups informing the user when the URL is copied to the clipboard.
-        // and only show our snackbar when Android will not show an indication by default.                 *
-        // See https://developer.android.com/develop/ui/views/touch-and-input/copy-paste#duplicate-notifications).
-        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
-            appStore.dispatch(ShareAction.CopyLinkToClipboard)
+        if (isPrivate) {
+            clipData.description.extras = PersistableBundle().apply {
+                putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true)
+            }
         }
+
+        clipboardManager.setPrimaryClip(clipData)
     }
 
     companion object {
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/ToolbarPopupWindow.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/ToolbarPopupWindow.kt
index 621e8e1f643..29585937137 100644
--- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/ToolbarPopupWindow.kt
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/ToolbarPopupWindow.kt
@@ -17,9 +17,11 @@ import androidx.annotation.VisibleForTesting
 import androidx.core.graphics.drawable.toDrawable
 import androidx.core.view.isVisible
 import mozilla.components.browser.state.selector.findCustomTab
+import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
 import mozilla.components.browser.state.selector.selectedTab
 import mozilla.components.browser.state.store.BrowserStore
 import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.utils.ClipboardHandler
 import mozilla.telemetry.glean.private.NoExtras
 import org.mozilla.fenix.GleanMetrics.Events
 import org.mozilla.fenix.R
@@ -83,30 +85,7 @@ object ToolbarPopupWindow {
         binding.pasteAndGo.isVisible = containsUrl && !isCustomTabSession
 
         if (copyVisible) {
-            binding.copy.setOnClickListener { copyView ->
-                popupWindow.dismiss()
-                clipboard.text = getUrlForClipboard(
-                    copyView.context.components.core.store,
-                    customTabId,
-                )
-
-                // Android 13+ shows by default a popup for copied text.
-                // Avoid overlapping popups informing the user when the URL is copied to the clipboard.
-                // and only show our snackbar when Android will not show an indication by default.                 *
-                // See https://developer.android.com/develop/ui/views/touch-and-input/copy-paste#duplicate-notifications).
-                if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
-                    snackbarParent.get()?.let { snackbarParent ->
-                        Snackbar.make(
-                            snackBarParentView = snackbarParent,
-                            snackbarState = SnackbarState(
-                                message = context.getString(R.string.browser_toolbar_url_copied_to_clipboard_snackbar),
-                                duration = SnackbarState.Duration.Preset.Long,
-                            ),
-                        ).show()
-                    }
-                }
-                Events.copyUrlTapped.record(NoExtras())
-            }
+            setupCopyButton(binding, popupWindow, clipboard, customTabId, snackbarParent, context)
         }
 
         if (binding.paste.isVisible) {
@@ -139,6 +118,42 @@ object ToolbarPopupWindow {
         }
     }
 
+    private fun setupCopyButton(
+        binding: BrowserToolbarPopupWindowBinding,
+        popupWindow: PopupWindow,
+        clipboard: ClipboardHandler,
+        customTabId: String?,
+        snackbarParent: WeakReference<ViewGroup>,
+        context: Context,
+    ) {
+        binding.copy.setOnClickListener { copyView ->
+            popupWindow.dismiss()
+            val store = copyView.context.components.core.store
+            val url = getUrlForClipboard(store, customTabId)
+            if (isPrivateTab(store, customTabId)) {
+                clipboard.sensitiveText = url
+            } else {
+                clipboard.text = url
+            }
+            // Android 13+ shows by default a popup for copied text.
+            // Avoid overlapping popups informing the user when the URL is copied to the clipboard.
+            // and only show our snackbar when Android will not show an indication by default.
+            // See https://developer.android.com/develop/ui/views/touch-and-input/copy-paste#duplicate-notifications).
+            if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
+                snackbarParent.get()?.let { snackbarParent ->
+                    Snackbar.make(
+                        snackBarParentView = snackbarParent,
+                        snackbarState = SnackbarState(
+                            message = context.getString(R.string.browser_toolbar_url_copied_to_clipboard_snackbar),
+                            duration = SnackbarState.Duration.Preset.Long,
+                        ),
+                    ).show()
+                }
+            }
+            Events.copyUrlTapped.record(NoExtras())
+        }
+    }
+
     /**
      * Calculates if the popup should be shown above or below the toolbar.
      */
@@ -169,4 +184,12 @@ object ToolbarPopupWindow {
             selectedTab?.readerState?.activeUrl ?: selectedTab?.content?.url
         }
     }
+
+    @VisibleForTesting
+    internal fun isPrivateTab(
+        store: BrowserStore,
+        customTabId: String? = null,
+    ): Boolean {
+        return store.state.findCustomTabOrSelectedTab(customTabId)?.content?.private ?: true
+    }
 }
Loading diff…