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
Tracker2054384
Fix commit37969182a870 (firefox) +71/-6
CISA KEVNot listed
CreditedAbdul Samad
Disclosed2026-09-29

Changed Functions

FunctionChangeNotes
if
mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/String.kt
modified

Files Changed

  • mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadNotification.kt
  • mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadNotificationTest.kt
  • mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/String.kt
  • mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/StringTest.kt
diff --git a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadNotification.kt b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadNotification.kt
index 75bffa56afa..1cd7cdf6769 100644
--- a/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadNotification.kt
+++ b/mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadNotification.kt
@@ -30,6 +30,7 @@ import mozilla.components.feature.downloads.AbstractFetchDownloadService.Compani
 import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.ACTION_RESUME
 import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.ACTION_TRY_AGAIN
 import mozilla.components.feature.downloads.AbstractFetchDownloadService.DownloadJobState
+import mozilla.components.support.ktx.kotlin.truncateMiddle
 import mozilla.components.support.utils.DownloadFileUtils
 
 @Suppress("LargeClass")
@@ -40,6 +41,7 @@ internal object DownloadNotification {
     internal const val NOTIFICATION_DOWNLOAD_GROUP_ID = 100
     private const val LEGACY_NOTIFICATION_CHANNEL_ID = "Downloads"
     internal const val PERCENTAGE_MULTIPLIER = 100
+    private const val MAX_FILENAME_LENGTH = 25
 
     @VisibleForTesting
     internal fun createDownloadGroupNotification(
@@ -89,11 +91,12 @@ internal object DownloadNotification {
         val channelId = ensureChannelExists(context)
         val isIndeterminate = downloadState.isIndeterminate()
         val percentCopied = downloadState.getPercent() ?: -1
+        val fileName = downloadState.fileName?.truncateMiddle(MAX_FILENAME_LENGTH).orEmpty()
 
         return NotificationCompat.Builder(context, channelId)
             .setStyle(
                 NotificationCompat.BigTextStyle()
-                    .setBigContentTitle(downloadState.fileName.orEmpty())
+                    .setBigContentTitle(fileName)
                     .setSummaryText(
                         formatDownloadTimeRemaining(
                             context = context,
@@ -105,7 +108,7 @@ internal object DownloadNotification {
                     )
             )
             .setSmallIcon(R.drawable.mozac_feature_download_ic_ongoing_download)
-            .setContentTitle(downloadState.fileName.orEmpty())
+            .setContentTitle(fileName)
             .setContentText(downloadState.getProgress(fileSizeFormatter = fileSizeFormatter))
             .setColor(ContextCompat.getColor(context, notificationAccentColor))
             .setCategory(NotificationCompat.CATEGORY_PROGRESS)
@@ -128,10 +131,11 @@ internal object DownloadNotification {
         notificationAccentColor: Int,
     ): Notification {
         val channelId = ensureChannelExists(context)
+        val fileName = downloadState.fileName?.truncateMiddle(MAX_FILENAME_LENGTH).orEmpty()
 
         return NotificationCompat.Builder(context, channelId)
             .setSmallIcon(R.drawable.mozac_feature_download_ic_download)
-            .setContentTitle(downloadState.fileName)
+            .setContentTitle(fileName)
             .setContentText(
                 context.applicationContext.getString(R.string.mozac_feature_downloads_paused_notification_text)
             )
@@ -157,10 +161,11 @@ internal object DownloadNotification {
         contentIntent: PendingIntent = createOpenFilePendingIntent(context, downloadState, downloadFileUtils),
     ): Notification {
         val channelId = ensureChannelExists(context)
+        val fileName = downloadState.fileName?.truncateMiddle(MAX_FILENAME_LENGTH).orEmpty()
 
         return NotificationCompat.Builder(context, channelId)
             .setSmallIcon(R.drawable.mozac_feature_download_ic_download_complete)
-            .setContentTitle(downloadState.fileName)
+            .setContentTitle(fileName)
             .setWhen(createdTime)
             .setOnlyAlertOnce(true)
             .setContentText(
@@ -183,10 +188,11 @@ internal object DownloadNotification {
         notificationAccentColor: Int,
     ): Notification {
         val channelId = ensureChannelExists(context)
+        val fileName = downloadState.fileName?.truncateMiddle(MAX_FILENAME_LENGTH).orEmpty()
 
         return NotificationCompat.Builder(context, channelId)
             .setSmallIcon(R.drawable.mozac_feature_download_ic_download_failed)
-            .setContentTitle(downloadState.fileName)
+            .setContentTitle(fileName)
             .setContentText(
                 context.applicationContext.getString(R.string.mozac_feature_downloads_failed_notification_text2)
             )
@@ -209,7 +215,8 @@ internal object DownloadNotification {
         notifications: List<DownloadJobState>,
     ): List<String> {
         return notifications.take(2).map { downloadState ->
-            "${downloadState.state.fileName} ${downloadState.state.getStatusDescription(
+            val fileName = downloadState.state.fileName.orEmpty().truncateMiddle(MAX_FILENAME_LENGTH)
+            "$fileName ${downloadState.state.getStatusDescription(
                 context = context,
                 fileSizeFormatter = fileSizeFormatter,
             )}"
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/String.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/String.kt
index da838d0de4c..379ce55213d 100644
--- a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/String.kt
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/String.kt
@@ -480,3 +480,24 @@ internal fun String.extractBase6RawString(): String? {
         contentString
     }
 }
+
+/**
+ * Truncates the string from the middle if it exceeds [maxLength].
+ *
+ * @param maxLength The maximum length of the string.
+ * @param ellipsis The string to use as ellipsis, defaults to "…".
+ * @return The truncated string.
+ */
+fun String.truncateMiddle(maxLength: Int, ellipsis: String = "…"): String {
+    if (length <= maxLength) {
+        return this
+    }
+    if (maxLength <= ellipsis.length) {
+        return take(maxLength)
+    }
+
+    val half = (maxLength - ellipsis.length) / 2
+    val start = take(half)
+    val end = takeLast(maxLength - ellipsis.length - half)
+    return "$start$ellipsis$end"
+}
diff --git a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadNotificationTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadNotificationTest.kt
index b7f3be776de..8c5fa35727b 100644
--- a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadNotificationTest.kt
+++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadNotificationTest.kt
@@ -4,6 +4,7 @@
 
 package mozilla.components.feature.downloads
 
+import android.app.Notification
 import android.app.PendingIntent
 import androidx.core.app.NotificationCompat
 import androidx.core.app.NotificationCompat.EXTRA_PROGRESS
@@ -312,6 +313,32 @@ class DownloadNotificationTest {
         assertEquals(true, notificationDownloadWithNoSize.extras.getBoolean(EXTRA_PROGRESS_INDETERMINATE))
     }
 
+    @Test
+    fun `createOngoingDownloadNotification with long filename truncates from middle`() {
+        val longFileName = "this_is_a_very_long_filename_that_exceeds_forty_characters_to_test_truncation.pdf"
+        val downloadState =
+            DownloadState(
+                fileName = longFileName,
+                url = "mozilla.org/file.pdf",
+                contentLength = 100L,
+                currentBytesCopied = 10,
+                status = DownloadState.Status.DOWNLOADING,
+            )
+        val style = AbstractFetchDownloadService.Style()
+
+        val notification =
+            DownloadNotification.createOngoingDownloadNotification(
+                context = testContext,
+                downloadState = downloadState,
+                fileSizeFormatter = fakeFileSizeFormatter,
+                notificationAccentColor = style.notificationAccentColor,
+                downloadEstimator = DownloadEstimator(dateTimeProvider = FakeDateTimeProvider()),
+            )
+
+        val expectedTitle = "this_is_a_ve…uncation.pdf"
+        assertEquals(expectedTitle, notification.extras.getCharSequence(Notification.EXTRA_TITLE).toString())
+    }
+
     @Test
     fun getOngoingNotificationAccentColor() {
         val download =
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/StringTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/StringTest.kt
index b51602a7068..5c2a57f201d 100644
--- a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/StringTest.kt
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/StringTest.kt
@@ -309,6 +309,16 @@ class StringTest {
         }
     }
 
+    @Test
+    fun truncateMiddle() {
+        assertEquals("123…890", "1234567890".truncateMiddle(7))
+        assertEquals("1234567890", "1234567890".truncateMiddle(10))
+        assertEquals("1234567890", "1234567890".truncateMiddle(20))
+        assertEquals("1…0", "1234567890".truncateMiddle(3))
+        assertEquals("1", "1234567890".truncateMiddle(1))
+        assertEquals("123...890", "1234567890".truncateMiddle(9, "..."))
+    }
+
     @Test
     fun `WHEN a string contains utf 8 encoded characters or illegal filename characters decode decodes it`() {
         // List of pairs of encoded strings or illegal filename characters and their expected decoded results
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/DownloadNotificationTest.kt b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadNotificationTest.kt
index b7f3be776de..8c5fa35727b 100644
--- a/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadNotificationTest.kt
+++ b/mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadNotificationTest.kt
@@ -4,6 +4,7 @@
 
 package mozilla.components.feature.downloads
 
+import android.app.Notification
 import android.app.PendingIntent
 import androidx.core.app.NotificationCompat
 import androidx.core.app.NotificationCompat.EXTRA_PROGRESS
@@ -312,6 +313,32 @@ class DownloadNotificationTest {
         assertEquals(true, notificationDownloadWithNoSize.extras.getBoolean(EXTRA_PROGRESS_INDETERMINATE))
     }
 
+    @Test
+    fun `createOngoingDownloadNotification with long filename truncates from middle`() {
+        val longFileName = "this_is_a_very_long_filename_that_exceeds_forty_characters_to_test_truncation.pdf"
+        val downloadState =
+            DownloadState(
+                fileName = longFileName,
+                url = "mozilla.org/file.pdf",
+                contentLength = 100L,
+                currentBytesCopied = 10,
+                status = DownloadState.Status.DOWNLOADING,
+            )
+        val style = AbstractFetchDownloadService.Style()
+
+        val notification =
+            DownloadNotification.createOngoingDownloadNotification(
+                context = testContext,
+                downloadState = downloadState,
+                fileSizeFormatter = fakeFileSizeFormatter,
+                notificationAccentColor = style.notificationAccentColor,
+                downloadEstimator = DownloadEstimator(dateTimeProvider = FakeDateTimeProvider()),
+            )
+
+        val expectedTitle = "this_is_a_ve…uncation.pdf"
+        assertEquals(expectedTitle, notification.extras.getCharSequence(Notification.EXTRA_TITLE).toString())
+    }
+
     @Test
     fun getOngoingNotificationAccentColor() {
         val download =
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/StringTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/StringTest.kt
index b51602a7068..5c2a57f201d 100644
--- a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/StringTest.kt
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/StringTest.kt
@@ -309,6 +309,16 @@ class StringTest {
         }
     }
 
+    @Test
+    fun truncateMiddle() {
+        assertEquals("123…890", "1234567890".truncateMiddle(7))
+        assertEquals("1234567890", "1234567890".truncateMiddle(10))
+        assertEquals("1234567890", "1234567890".truncateMiddle(20))
+        assertEquals("1…0", "1234567890".truncateMiddle(3))
+        assertEquals("1", "1234567890".truncateMiddle(1))
+        assertEquals("123...890", "1234567890".truncateMiddle(9, "..."))
+    }
+
     @Test
     fun `WHEN a string contains utf 8 encoded characters or illegal filename characters decode decodes it`() {
         // List of pairs of encoded strings or illegal filename characters and their expected decoded results
Loading diff…