CVE-2026-16373
Overview
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-componentshelper wrapping the Android clipboard, exposing bothtextand asensitiveTextsetter 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.privateboolean 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.
content.private and uses sensitiveText / EXTRA_IS_SENSITIVE for private tabs, defaulting to sensitive when the state is unknown.Attack Path
- Browse privately The victim opens a URL in a private tab that they expect to remain confidential.
- 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.
- System exposure On Android 13+ the OS renders the un-flagged URL in the copy-confirmation preview popup, revealing it on screen.
- Clipboard observation Any other installed app able to read the primary clipboard retrieves the private URL because it was never marked sensitive.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifmobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarMiddleware.kt |
modified | |
ifmobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/CustomTabBrowserToolbarMiddleware.kt |
modified | |
ifmobile/android/fenix/app/src/main/java/org/mozilla/fenix/share/ShareController.kt |
modified | |
ifmobile/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.ktmobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/CustomTabBrowserToolbarMiddleware.ktmobile/android/fenix/app/src/main/java/org/mozilla/fenix/share/ShareController.ktmobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/ToolbarPopupWindow.kt
Audit Directions
- Clipboard writes of session datagrep for
clipboard.text,setPrimaryClip, andClipData.newPlainTextand confirm each site marks private-session content sensitive viasensitiveTextorEXTRA_IS_SENSITIVE. - Fail-safe default on private stateverify that any check of
content.privatetreatsnullor unknown as private (defaulting to sensitive) rather than assuming a page is non-private. - Private-state propagation to platform sinksaudit 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.
Patch
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
+ }
}