CVE-2026-74957
Overview
Background
- Safe Browsing / Application Reputation
- Firefox’s
ApplicationReputationcomponent 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
StringEndsWithand 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.
Attack Path
- 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.
- Case-mangle the extension
The attacker names the file with a non-lowercase extension (for example
Installer.DMGorbundle.ZIP) that the OS still opens normally. - Deliver the download
The victim is lured into downloading the file through a web page or link so it reaches
ApplicationReputation. - Classification miss
The case-sensitive
StringEndsWithchecks inGetDownloadType/GetFileExtfail to recognize the extension, so the file is not assigned its executable/binaryDownloadType. - 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
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
| Function | Change | Notes |
|---|---|---|
fortoolkit/components/reputationservice/ApplicationReputation.cpp |
modified |
Files Changed
toolkit/components/reputationservice/ApplicationReputation.cpp
Audit Directions
- Case-sensitive security comparisonsAudit any allowlist/denylist or classification that matches extensions, schemes, hostnames, or MIME types with default case-sensitive
StringEndsWith/Equalsand confirm case normalization on the security path. - Extension-to-classification parityVerify 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 normalizationReview filename and identifier handling where attacker-supplied casing, Unicode folding, or trailing characters could cause a check and the OS/consumer to disagree.
Patch
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;
}