Medium firefox Logic Error 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionMitigation bypass in the Safe Browsing component
ComponentToolkit
Bug ClassLogic Error
Tracker2041906
Fix commitdf4f46d77bb8 (firefox) +34/-28
CISA KEVNot listed
CreditedTomoya Nakanishi
Disclosed2026-08-18

Background

Safe Browsing / Application Reputation
Firefox’s ApplicationReputation component that classifies downloaded files and queries Google’s reputation service to warn users about malicious binaries.
`StringEndsWith`
a Gecko string helper that tests whether a string ends with a given suffix, defaulting to a case-sensitive comparison unless a comparator is supplied.
`nsCaseInsensitiveCStringComparator`
a comparator object that makes StringEndsWith and similar string operations match without regard to letter case.
`ClientDownloadRequest::DownloadType`
the enum tag the reputation request assigns to a download (e.g. ZIPPED_EXECUTABLE, ANDROID_APK, MAC_EXECUTABLE) based on the file’s extension.

Root Cause Analysis

Both GetFileExt and PendingLookup::GetDownloadType in ApplicationReputation.cpp matched a download’s filename extension using StringEndsWith with the default case-sensitive comparator. Because filesystem and web extensions are conventionally case-insensitive, a file named with an upper- or mixed-case extension such as evil.DMG or payload.Zip would fail every literal lowercase suffix comparison, so the file was not tagged with its correct DownloadType and could slip past the reputation classification that these tags feed. The invariant violated is that extension matching in the security-relevant classification path must be case-insensitive, matching how the operating system and users actually treat those extensions.

The fix makes GetFileExt pass nsCaseInsensitiveCStringComparator and has GetDownloadType compare against a ToLowerCase-normalized copy (fileName) of the filename, so any casing of a known extension now maps to the same classification. This restores the intended coverage of the Safe Browsing mitigation for all extension spellings.

Key insight
The single core mistake was performing security-sensitive extension matching with case-sensitive string comparisons, letting an attacker evade classification simply by altering the extension’s letter case; the fix normalizes case (a case-insensitive comparator and a lowercased filename copy) before matching.

Attack Path

  1. Craft a payload An attacker prepares a malicious binary in a format Safe Browsing would normally flag, such as a macOS disk image or a ZIP archive.
  2. Case-mangle the extension The attacker names the file with a non-lowercase extension (for example Installer.DMG or bundle.ZIP) that the OS still opens normally.
  3. Deliver the download The victim is lured into downloading the file through a web page or link so it reaches ApplicationReputation.
  4. Classification miss The case-sensitive StringEndsWith checks in GetDownloadType/GetFileExt fail to recognize the extension, so the file is not assigned its executable/binary DownloadType.
  5. Mitigation bypass The reputation-based warning that depends on correct classification is weakened or skipped, and the potentially malicious file is treated with less scrutiny.

Impact Assessment

An attacker gains a mitigation bypass of the Safe Browsing download-reputation check, reducing the chance the user is warned about a malicious download. The weakness lives in the browser-process ApplicationReputation toolkit component and affects downloads whose extensions are spelled in non-lowercase form. The precondition is that the attacker gets the victim to download an attacker-named file; the issue only degrades a protective warning rather than granting direct code execution, consistent with the medium/moderate rating.

Changed Functions

FunctionChangeNotes
for
toolkit/components/reputationservice/ApplicationReputation.cpp
modified

Files Changed

  • toolkit/components/reputationservice/ApplicationReputation.cpp

Audit Directions

  • Case-sensitive security comparisons
    Audit any allowlist/denylist or classification that matches extensions, schemes, hostnames, or MIME types with default case-sensitive StringEndsWith/Equals and confirm case normalization on the security path.
  • Extension-to-classification parity
    Verify that every place mapping filenames to download or content types uses the same case-insensitive rules, so no code path re-introduces a divergence an attacker can exploit.
  • Attacker-controlled normalization
    Review filename and identifier handling where attacker-supplied casing, Unicode folding, or trailing characters could cause a check and the OS/consumer to disagree.
diff --git a/toolkit/components/reputationservice/ApplicationReputation.cpp b/toolkit/components/reputationservice/ApplicationReputation.cpp
index 016e3c2ca0e..09a409f6bbd 100644
--- a/toolkit/components/reputationservice/ApplicationReputation.cpp
+++ b/toolkit/components/reputationservice/ApplicationReputation.cpp
@@ -936,7 +936,8 @@ static const char* GetFileExt(const nsACString& aFilename,
                               const char* const aFileExtensions[],
                               const size_t aLength) {
   for (size_t i = 0; i < aLength; ++i) {
-    if (StringEndsWith(aFilename, nsDependentCString(aFileExtensions[i]))) {
+    if (StringEndsWith(aFilename, nsDependentCString(aFileExtensions[i]),
+                       nsCaseInsensitiveCStringComparator)) {
       return aFileExtensions[i];
     }
   }
@@ -976,37 +977,42 @@ ClientDownloadRequest::DownloadType PendingLookup::GetDownloadType(
     const nsACString& aFilename) {
   MOZ_ASSERT(IsBinary(aFilename));
 
+  // Extensions are matched case-insensitively, so compare against a lowercased
+  // copy of the filename.
+  nsAutoCString fileName(aFilename);
+  ToLowerCase(fileName);
+
   // From
   // https://cs.chromium.org/chromium/src/chrome/common/safe_browsing/download_protection_util.cc?l=17
-  if (StringEndsWith(aFilename, ".zip"_ns)) {
+  if (StringEndsWith(fileName, ".zip"_ns)) {
     return ClientDownloadRequest::ZIPPED_EXECUTABLE;
-  } else if (StringEndsWith(aFilename, ".apk"_ns)) {
+  } else if (StringEndsWith(fileName, ".apk"_ns)) {
     return ClientDownloadRequest::ANDROID_APK;
-  } else if (StringEndsWith(aFilename, ".app"_ns) ||
-             StringEndsWith(aFilename, ".applescript"_ns) ||
-             StringEndsWith(aFilename, ".cdr"_ns) ||
-             StringEndsWith(aFilename, ".dart"_ns) ||
-             StringEndsWith(aFilename, ".dc42"_ns) ||
-             StringEndsWith(aFilename, ".diskcopy42"_ns) ||
-             StringEndsWith(aFilename, ".dmg"_ns) ||
-             StringEndsWith(aFilename, ".dmgpart"_ns) ||
-             StringEndsWith(aFilename, ".dvdr"_ns) ||
-             StringEndsWith(aFilename, ".img"_ns) ||
-             StringEndsWith(aFilename, ".imgpart"_ns) ||
-             StringEndsWith(aFilename, ".iso"_ns) ||
-             StringEndsWith(aFilename, ".mpkg"_ns) ||
-             StringEndsWith(aFilename, ".ndif"_ns) ||
-             StringEndsWith(aFilename, ".osas"_ns) ||
-             StringEndsWith(aFilename, ".osax"_ns) ||
-             StringEndsWith(aFilename, ".pkg"_ns) ||
-             StringEndsWith(aFilename, ".scpt"_ns) ||
-             StringEndsWith(aFilename, ".scptd"_ns) ||
-             StringEndsWith(aFilename, ".seplugin"_ns) ||
-             StringEndsWith(aFilename, ".smi"_ns) ||
-             StringEndsWith(aFilename, ".sparsebundle"_ns) ||
-             StringEndsWith(aFilename, ".sparseimage"_ns) ||
-             StringEndsWith(aFilename, ".toast"_ns) ||
-             StringEndsWith(aFilename, ".udif"_ns)) {
+  } else if (StringEndsWith(fileName, ".app"_ns) ||
+             StringEndsWith(fileName, ".applescript"_ns) ||
+             StringEndsWith(fileName, ".cdr"_ns) ||
+             StringEndsWith(fileName, ".dart"_ns) ||
+             StringEndsWith(fileName, ".dc42"_ns) ||
+             StringEndsWith(fileName, ".diskcopy42"_ns) ||
+             StringEndsWith(fileName, ".dmg"_ns) ||
+             StringEndsWith(fileName, ".dmgpart"_ns) ||
+             StringEndsWith(fileName, ".dvdr"_ns) ||
+             StringEndsWith(fileName, ".img"_ns) ||
+             StringEndsWith(fileName, ".imgpart"_ns) ||
+             StringEndsWith(fileName, ".iso"_ns) ||
+             StringEndsWith(fileName, ".mpkg"_ns) ||
+             StringEndsWith(fileName, ".ndif"_ns) ||
+             StringEndsWith(fileName, ".osas"_ns) ||
+             StringEndsWith(fileName, ".osax"_ns) ||
+             StringEndsWith(fileName, ".pkg"_ns) ||
+             StringEndsWith(fileName, ".scpt"_ns) ||
+             StringEndsWith(fileName, ".scptd"_ns) ||
+             StringEndsWith(fileName, ".seplugin"_ns) ||
+             StringEndsWith(fileName, ".smi"_ns) ||
+             StringEndsWith(fileName, ".sparsebundle"_ns) ||
+             StringEndsWith(fileName, ".sparseimage"_ns) ||
+             StringEndsWith(fileName, ".toast"_ns) ||
+             StringEndsWith(fileName, ".udif"_ns)) {
     return ClientDownloadRequest::MAC_EXECUTABLE;
   }
 
Loading diff…