Medium CVSS 5.3 firefox Logic Error 🔧 Commit mapped

Overview

Medium
Severity
5.3
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionA specially crafted filename containing a large number of spaces could obscure the file's extension when displayed in the download dialog.<br>*This bug only affects Firefox for Android. Other versions of Firefox are unaffected.*
ComponentCore
Bug ClassLogic Error
Tracker1906024
Fix commitaeb8e9b29d78 (firefox) +19/-7
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N
CISA KEVNot listed
CreditedHafiizh
Disclosed2024-10-01

Background

`sanitizeFileName`
a Kotlin String extension in String.kt that cleans a proposed download filename before it is shown in the Android download dialog.
`replaceEscapedCharacters`
a helper that maps control and reserved characters (matched by escapedCharactersRegex) to the UNDERSCORE character so they cannot corrupt or disguise the name.
Extension-spoofing
a display attack where whitespace or other filler is packed before the extension so the user sees a benign-looking name while the real type (the trailing extension) is scrolled or clipped out of view.

Root Cause Analysis

The vulnerable code path is sanitizeFileName, which normalized dots and then only called replaceEscapedCharacters before returning the name for display in the download dialog.

The invariant that was violated is that a sanitized filename should present its true extension to the user without misleading padding, but replaceEscapedCharacters’s regex escapedCharactersRegex ([\x00-\x13*"?<>:|\\]) collapsed several whitespace control characters (tab \u0009, line feed \u000A, vertical tab \u000B, form feed \u000C, carriage return \u000D) into UNDERSCORE yet did nothing about ordinary spaces, so a name with a long run of spaces such as invoice.pdf<many spaces>.apk could push the real extension out of the visible field.

The fix adds replaceContinuousSpaces, which uses the \s+ regex to collapse any run of whitespace down to a single SPACE, and chains a trailing trim(), so no amount of padding survives to obscure the extension. Because whitespace is now folded rather than preserved, the displayed name faithfully reflects the file’s true extension, defeating the display-obfuscation trick.

Key insight
The single core mistake was that sanitizeFileName treated only reserved/control characters as dangerous while leaving arbitrary runs of spaces intact, letting a filename visually hide its extension; the fix adds replaceContinuousSpaces plus a trim() so continuous whitespace is normalized to one SPACE and the extension can no longer be pushed out of view.

Attack Path

  1. Craft a padded filename The attacker names a hosted file so a legitimate-looking extension is followed by a long run of spaces and then the real, dangerous extension (e.g. photo.jpg + hundreds of spaces + .apk).
  2. Serve to Firefox for Android A victim on Firefox for Android is induced to download the file, and the server-supplied name flows into sanitizeFileName.
  3. Whitespace survives sanitization Because replaceEscapedCharacters only handled control characters and not spaces, the many-space run passed through unchanged into the name used for display.
  4. Dialog obscures the extension In the download dialog the visible portion shows only the benign prefix while the true trailing extension is scrolled or clipped beyond the field.
  5. User approves under false pretenses Believing the file is the harmless type shown, the user accepts the download of a file whose real extension is malicious.

Impact Assessment

An attacker gains the ability to visually spoof a download’s extension in the Firefox for Android download dialog, increasing the chance a user saves and opens a file of a different, dangerous type than they believe. This occurs in the browser UI / download-handling context on Android only, and the preconditions are that the victim uses Firefox for Android and is convinced to download an attacker-named file. The impact is rated moderate because it relies on user interaction and social engineering rather than granting direct code execution.

Files Changed

  • 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

Audit Directions

  • Display-vs-storage divergence
    Look for any place a user-controlled string is sanitized for safety characters but not normalized for whitespace or bidi/format characters before being shown in a security-relevant UI element.
  • Regex allowlist gaps
    Review character-class regexes like escapedCharactersRegex for excluded but abusable characters (plain spaces, Unicode whitespace, RTL overrides) that fall outside the intended match set.
  • Truncated UI fields
    Audit dialogs and labels that can scroll or clip long values, where a trailing extension or suffix could be hidden by leading padding, and confirm inputs are trimmed and collapsed first.
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 ae9d4393a94..192120bd914 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
@@ -53,6 +53,8 @@ const val MAX_URI_LENGTH = 25000
 
 private const val FILE_PREFIX = "file://"
 private const val MAX_VALID_PORT = 65_535
+private const val SPACE = " "
+private const val UNDERSCORE = "_"
 
 /**
  * Shortens URLs to be more user friendly.
@@ -307,7 +309,9 @@ fun String.sanitizeFileName(): String {
         file.name.replace("\\.\\.+".toRegex(), ".")
     } else {
         file.name.replace(".", "")
-    }.replaceEscapedCharacters()
+    }.replaceContinuousSpaces()
+        .replaceEscapedCharacters()
+        .trim()
 }
 
 /**
@@ -316,7 +320,15 @@ fun String.sanitizeFileName(): String {
  */
 private fun String.replaceEscapedCharacters(): String {
     val escapedCharactersRegex = "[\\x00-\\x13*\"?<>:|\\\\]".toRegex()
-    return replace(escapedCharactersRegex, "_")
+    return replace(escapedCharactersRegex, UNDERSCORE)
+}
+
+/**
+ * Replaces continuous spaces with a single space.
+ */
+private fun String.replaceContinuousSpaces(): String {
+    val escapedCharactersRegex = "\\s+".toRegex()
+    return replace(escapedCharactersRegex, SPACE)
 }
 
 /**
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 12ee4beab6b..51ed86a70f8 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
@@ -199,11 +199,11 @@ class StringTest {
             "acknowledge\u0006signal" to "acknowledge_signal",
             "bell\u0007sound" to "bell_sound",
             "back\u0008space" to "back_space",
-            "horizontal\u0009tab" to "horizontal_tab",
-            "new\u000Aline" to "new_line",
-            "vertical\u000Btab" to "vertical_tab",
-            "form\u000Cfeed" to "form_feed",
-            "return\u000Dcarriage" to "return_carriage",
+            "horizontal\u0009tab" to "horizontal tab",
+            "new\u000Aline" to "new line",
+            "vertical\u000Btab" to "vertical tab",
+            "form\u000Cfeed" to "form feed",
+            "return\u000Dcarriage" to "return carriage",
             "shift\u000Eout" to "shift_out",
             "shift\u000Fin" to "shift_in",
             "escape\u0010data" to "escape_data",
Loading diff…

Regression Test / PoC

shipped with the fix
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 12ee4beab6b..51ed86a70f8 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
@@ -199,11 +199,11 @@ class StringTest {
             "acknowledge\u0006signal" to "acknowledge_signal",
             "bell\u0007sound" to "bell_sound",
             "back\u0008space" to "back_space",
-            "horizontal\u0009tab" to "horizontal_tab",
-            "new\u000Aline" to "new_line",
-            "vertical\u000Btab" to "vertical_tab",
-            "form\u000Cfeed" to "form_feed",
-            "return\u000Dcarriage" to "return_carriage",
+            "horizontal\u0009tab" to "horizontal tab",
+            "new\u000Aline" to "new line",
+            "vertical\u000Btab" to "vertical tab",
+            "form\u000Cfeed" to "form feed",
+            "return\u000Dcarriage" to "return carriage",
             "shift\u000Eout" to "shift_out",
             "shift\u000Fin" to "shift_in",
             "escape\u0010data" to "escape_data",
Loading diff…