CVE-2025-1936
Overview
Background
- jar:/omnijar entry lookup
- Firefox packages built-in resources (and, at runtime, web-extension content) inside ZIP archives.
nsZipArchive::GetItemlocates an entry by name; callers obtain it throughjar:URLs,nsJAR,FileLocation,URLPreloader, the font list, the hyphenator, etc. - `HashName` / `memcmp` lookup
GetItemhashes the firstlenbytes of the name and compareslenbytes withmemcmpagainst eachnsZipItem::Name(). The value oflentherefore decides how much of the requested name participates in the match.- Content-type sniffing split
- The caller that retrieves the archive member and the code that decides the member’s MIME type can look at different representations of the same name; CVE-2025-1936 is about them disagreeing.
Root Cause Analysis
Every entry lookup in the jar subsystem funneled a Gecko nsCString through a const char* boundary: zip->GetItem(aFile.get()), mZip->GetItem(PromiseFlatCString(aEntryName).get()), reader->GetItem(mFilename.get()), etc. Inside nsZipArchive::GetItem(const char* aEntryName) the length was recomputed with strlen(aEntryName). Because strlen stops at the first NUL, an entry name such as evil.js\0.png was truncated to evil.js for the purpose of locating and reading the archive member, while other code paths that kept the full counted string (for example type determination from the trailing .png) saw the fake extension after the NUL. The two views of the same name diverged, so the content actually fetched from the ZIP (the real .js) could be treated as an image. The advisory (CWE-158, “Improper Null Termination”) frames this as content being hidden in an extension disguised as an image.
The patch removes the const char* boundary wholesale: nsZipArchive::GetItem, nsZipArchive::Test, nsZipItemPtr_base, nsZipHandle::Init, and the FileLocation constructors/Init all take const nsACString&. GetItem now uses aEntryName.Length() for len and aEntryName.BeginReading() for hashing and memcmp, so the entire counted name — bytes after any embedded NUL included — participates in the lookup. Callers pass the nsCString/nsACString directly (zip->GetItem(aFile), nsDependentCString(mArray[...].get(), mArray[...].Length()) in nsJARInputStream::ReadDirectory), eliminating the truncation and forcing the lookup key and the sniffed name to be the same bytes.
strlen/.get(), so the bytes after the NUL silently changed the apparent file type without changing which member was actually read.Attack Path
- Package author
Builds a web extension (or any
jar:-reachable archive) with a ZIP member whose stored name embeds a NUL, e.g.payload.js\x00.png. - Type gate
A consumer that inspects the full counted name sees the trailing
.pngand classifies the resource as an image, passing content-type / allow-list checks (inferred: the exact sniffing caller is not in this diff). - Retrieval
GetItemis reached with the same name;strlentruncates it topayload.js, so the archive returns the JavaScript member. - Mismatch exploited The JavaScript bytes are delivered under the image classification, hiding executable code behind a benign-looking extension.
- Primitive Content-type confusion / smuggling of code into a context that expected an image, enabling review/allow-list evasion for extension content.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifdom/xhr/XMLHttpRequestMainThread.cpp |
modified | |
ifgfx/thebes/gfxFT2FontList.cpp |
modified | |
ifintl/hyphenation/glue/nsHyphenator.cpp |
modified | |
ifintl/locale/LocaleService.cpp |
modified | |
ifjs/xpconnect/loader/URLPreloader.cpp |
modified | |
ifmodules/libjar/nsJAR.cpp |
modified | |
ifmodules/libjar/nsZipArchive.cpp |
modified | |
whilemodules/libjar/nsZipArchive.cpp |
modified | |
FileLocationxpcom/build/FileLocation.cpp |
modified | |
ifxpcom/build/FileLocation.cpp |
modified |
Files Changed
dom/xhr/XMLHttpRequestMainThread.cppgfx/thebes/gfxFT2FontList.cppintl/hyphenation/glue/nsHyphenator.cppintl/locale/LocaleService.cppjs/xpconnect/loader/URLPreloader.cppmodules/libjar/nsJAR.cppmodules/libjar/nsJARInputStream.cppmodules/libjar/nsZipArchive.cppmodules/libjar/nsZipArchive.htoolkit/mozapps/extensions/AddonManagerStartup.cppxpcom/build/FileLocation.cppxpcom/build/FileLocation.hxpcom/build/Omnijar.cppxpcom/components/nsComponentManager.cpp
Audit Directions
- Other `.get()` into lookup/compareGrep for
GetItem(,Test(, and similar archive/table lookups still receiving.get()orPromiseFlatCString(...).get(); any remainingconst char*entry point can reintroduce NUL truncation. - Name vs type splitReview every place that derives a MIME type or extension from an entry name independently of the bytes used to fetch it (extension packaging,
nsIContentSniffercallers), ensuring both use the identical counted string. - Embedded-NUL handling elsewhereAudit URL/path parsers and ZIP/central-directory readers (
nsZipArchive::BuildFileList) forstrlen-style length recomputation on names that originate from attacker-controlled archives.
Patch
diff --git a/dom/xhr/XMLHttpRequestMainThread.cpp b/dom/xhr/XMLHttpRequestMainThread.cpp
index 59352f1e716..e9472466d26 100644
--- a/dom/xhr/XMLHttpRequestMainThread.cpp
+++ b/dom/xhr/XMLHttpRequestMainThread.cpp
@@ -4182,7 +4182,7 @@ nsresult ArrayBufferBuilder::MapToFileInPackage(const nsCString& aFile,
if (!zip) {
return NS_ERROR_FAILURE;
}
- nsZipItem* zipItem = zip->GetItem(aFile.get());
+ nsZipItem* zipItem = zip->GetItem(aFile);
if (!zipItem) {
return NS_ERROR_FILE_NOT_FOUND;
}
diff --git a/gfx/thebes/gfxFT2FontList.cpp b/gfx/thebes/gfxFT2FontList.cpp
index 5c94e81d9be..13a32ef76fd 100644
--- a/gfx/thebes/gfxFT2FontList.cpp
+++ b/gfx/thebes/gfxFT2FontList.cpp
@@ -103,7 +103,7 @@ already_AddRefed<SharedFTFace> FT2FontEntry::GetFTFace(bool aCommit) {
RefPtr<SharedFTFace> face;
if (mFilename[0] != '/') {
RefPtr<nsZipArchive> reader = Omnijar::GetReader(Omnijar::Type::GRE);
- nsZipItem* item = reader->GetItem(mFilename.get());
+ nsZipItem* item = reader->GetItem(mFilename);
NS_ASSERTION(item, "failed to find zip entry");
uint32_t bufSize = item->RealSize();
@@ -468,7 +468,7 @@ hb_face_t* FT2FontEntry::CreateHBFace() const {
// A relative path means an omnijar resource, which we may need to
// decompress to a temporary buffer.
RefPtr<nsZipArchive> reader = Omnijar::GetReader(Omnijar::Type::GRE);
- nsZipItem* item = reader->GetItem(mFilename.get());
+ nsZipItem* item = reader->GetItem(mFilename);
MOZ_ASSERT(item, "failed to find zip entry");
if (item) {
// TODO(jfkthame):
@@ -1510,7 +1510,7 @@ void gfxFT2FontList::AppendFacesFromOmnijarEntry(nsZipArchive* aArchive,
}
}
- nsZipItem* item = aArchive->GetItem(aEntryName.get());
+ nsZipItem* item = aArchive->GetItem(aEntryName);
NS_ASSERTION(item, "failed to find zip entry");
uint32_t bufSize = item->RealSize();
diff --git a/intl/hyphenation/glue/nsHyphenator.cpp b/intl/hyphenation/glue/nsHyphenator.cpp
index e506a3a423d..40ce45018b9 100644
--- a/intl/hyphenation/glue/nsHyphenator.cpp
+++ b/intl/hyphenation/glue/nsHyphenator.cpp
@@ -52,7 +52,7 @@ static const void* GetItemPtrFromJarURI(nsIJARURI* aJAR, uint32_t* aLength) {
if (archive) {
nsCString path;
aJAR->GetJAREntry(path);
- nsZipItem* item = archive->GetItem(path.get());
+ nsZipItem* item = archive->GetItem(path);
if (item && item->Compression() == 0 && item->Size() > 0) {
// We do NOT own this data, but it won't go away until the omnijar
// file is closed during shutdown.
diff --git a/intl/locale/LocaleService.cpp b/intl/locale/LocaleService.cpp
index 1527f279103..7cb42fbb11c 100644
--- a/intl/locale/LocaleService.cpp
+++ b/intl/locale/LocaleService.cpp
@@ -338,7 +338,7 @@ static bool GetGREFileContents(const char* aFilePath, nsCString* aOutString) {
// Look for the requested file in omnijar.
RefPtr<nsZipArchive> zip = Omnijar::GetReader(Omnijar::GRE);
if (zip) {
- nsZipItemPtr<char> item(zip, aFilePath);
+ nsZipItemPtr<char> item(zip, nsDependentCString(aFilePath));
if (!item) {
return false;
}
diff --git a/js/xpconnect/loader/URLPreloader.cpp b/js/xpconnect/loader/URLPreloader.cpp
index 1aa18aca44f..6721cd04711 100644
--- a/js/xpconnect/loader/URLPreloader.cpp
+++ b/js/xpconnect/loader/URLPreloader.cpp
@@ -402,7 +402,7 @@ void URLPreloader::BackgroundReadFiles() {
entry->TypeString(), entry->mPath.get());
}
- auto item = zip->GetItem(entry->mPath.get());
+ auto item = zip->GetItem(entry->mPath);
if (!item) {
entry->mResultCode = NS_ERROR_FILE_NOT_FOUND;
continue;
@@ -567,7 +567,7 @@ Result<nsCString, nsresult> URLPreloader::ReadURIInternal(nsIURI* uri,
}
// Not an Omnijar archive, so just read it directly.
- FileLocation location(zip, PromiseFlatCString(path).BeginReading());
+ FileLocation location(zip, path);
return URLEntry::ReadLocation(location);
}
@@ -636,7 +636,7 @@ Result<FileLocation, nsresult> URLPreloader::CacheKey::ToFileLocation() {
}
RefPtr<nsZipArchive> zip = Archive();
- return FileLocation(zip, mPath.get());
+ return FileLocation(zip, mPath);
}
Result<nsCString, nsresult> URLPreloader::URLEntry::Read() {
diff --git a/modules/libjar/nsJAR.cpp b/modules/libjar/nsJAR.cpp
index bc413190705..8193e72563f 100644
--- a/modules/libjar/nsJAR.cpp
+++ b/modules/libjar/nsJAR.cpp
@@ -135,8 +135,7 @@ nsJAR::OpenInner(nsIZipReader* aZipReader, const nsACString& aZipEntry) {
{
nsJAR* outerJAR = static_cast<nsJAR*>(aZipReader);
RecursiveMutexAutoLock outerLock(outerJAR->mLock);
- rv = nsZipHandle::Init(outerJAR->mZip.get(),
- PromiseFlatCString(aZipEntry).get(),
+ rv = nsZipHandle::Init(outerJAR->mZip.get(), aZipEntry,
getter_AddRefs(handle));
NS_ENSURE_SUCCESS(rv, rv);
}
@@ -191,8 +190,7 @@ nsJAR::Test(const nsACString& aEntryName) {
if (!mZip) {
return NS_ERROR_FAILURE;
}
- return mZip->Test(
- aEntryName.IsEmpty() ? nullptr : PromiseFlatCString(aEntryName).get());
+ return mZip->Test(aEntryName);
}
NS_IMETHODIMP
@@ -205,7 +203,7 @@ nsJAR::Extract(const nsACString& aEntryName, nsIFile* outFile) {
}
LOG(("Extract[%p] %s", this, PromiseFlatCString(aEntryName).get()));
- nsZipItem* item = mZip->GetItem(PromiseFlatCString(aEntryName).get());
+ nsZipItem* item = mZip->GetItem(aEntryName);
NS_ENSURE_TRUE(item, NS_ERROR_FILE_NOT_FOUND);
// Remove existing file or directory so we set permissions correctly.
@@ -245,7 +243,7 @@ nsJAR::GetEntry(const nsACString& aEntryName, nsIZipEntry** result) {
if (!mZip) {
return NS_ERROR_FAILURE;
}
- nsZipItem* zipItem = mZip->GetItem(PromiseFlatCString(aEntryName).get());
+ nsZipItem* zipItem = mZip->GetItem(aEntryName);
NS_ENSURE_TRUE(zipItem, NS_ERROR_FILE_NOT_FOUND);
RefPtr<nsJARItem> jarItem = new nsJARItem(zipItem);
@@ -261,7 +259,7 @@ nsJAR::HasEntry(const nsACString& aEntryName, bool* result) {
if (!mZip) {
return NS_ERROR_FAILURE;
}
- *result = mZip->GetItem(PromiseFlatCString(aEntryName).get()) != nullptr;
+ *result = mZip->GetItem(aEntryName) != nullptr;
return NS_OK;
}
@@ -301,7 +299,7 @@ nsJAR::GetInputStream(const nsACString& aEntryName, nsIInputStream** result) {
const nsCString& entry = PromiseFlatCString(aEntryName);
if (*entry.get()) {
// First check if item exists in jar
- item = mZip->GetItem(entry.get());
+ item = mZip->GetItem(entry);
if (!item) return NS_ERROR_FILE_NOT_FOUND;
}
RefPtr<nsJARInputStream> jis = new nsJARInputStream();
diff --git a/modules/libjar/nsJARInputStream.cpp b/modules/libjar/nsJARInputStream.cpp
index 83f8f3c224e..8095f9edf12 100644
--- a/modules/libjar/nsJARInputStream.cpp
+++ b/modules/libjar/nsJARInputStream.cpp
@@ -341,7 +341,8 @@ nsresult nsJARInputStream::ReadDirectory(char* aBuffer, uint32_t aCount,
const char* entryName = mArray[mArrPos].get();
uint32_t entryNameLen = mArray[mArrPos].Length();
- nsZipItem* ze = mJar->mZip->GetItem(entryName);
+ nsZipItem* ze = mJar->mZip->GetItem(
+ nsDependentCString(mArray[mArrPos].get(), mArray[mArrPos].Length()));
NS_ENSURE_TRUE(ze, NS_ERROR_FILE_NOT_FOUND);
// Last Modified Time
diff --git a/modules/libjar/nsZipArchive.cpp b/modules/libjar/nsZipArchive.cpp
index cf894dc9d1e..dcbd9327b92 100644
--- a/modules/libjar/nsZipArchive.cpp
+++ b/modules/libjar/nsZipArchive.cpp
@@ -237,12 +237,12 @@ nsresult nsZipHandle::Init(nsIFile* file, nsZipHandle** ret, PRFileDesc** aFd) {
return NS_OK;
}
-nsresult nsZipHandle::Init(nsZipArchive* zip, const char* entry,
+nsresult nsZipHandle::Init(nsZipArchive* zip, const nsACString& entry,
nsZipHandle** ret) {
RefPtr<nsZipHandle> handle = new nsZipHandle();
if (!handle) return NS_ERROR_OUT_OF_MEMORY;
- LOG(("ZipHandle::Init entry %s", entry));
+ LOG(("ZipHandle::Init entry %s", PromiseFlatCString(entry).get()));
nsZipItem* item = zip->GetItem(entry);
if (item && item->Compression() == DEFLATED &&
@@ -414,10 +414,10 @@ already_AddRefed<nsZipArchive> nsZipArchive::OpenArchive(nsIFile* aFile) {
//---------------------------------------------
// nsZipArchive::Test
//---------------------------------------------
-nsresult nsZipArchive::Test(const char* aEntryName) {
+nsresult nsZipArchive::Test(const nsACString& aEntryName) {
nsZipItem* currItem;
- if (aEntryName) // only test specified item
+ if (aEntryName.Length()) // only test specified item
{
currItem = GetItem(aEntryName);
if (!currItem) return NS_ERROR_FILE_NOT_FOUND;
@@ -442,12 +442,13 @@ nsresult nsZipArchive::Test(const char* aEntryName) {
//---------------------------------------------
// nsZipArchive::GetItem
//---------------------------------------------
-nsZipItem* nsZipArchive::GetItem(const char* aEntryName) {
+nsZipItem* nsZipArchive::GetItem(const nsACString& aEntryName) {
MutexAutoLock lock(mLock);
- LOG(("ZipHandle::GetItem[%p] %s", this, aEntryName));
- if (aEntryName) {
- uint32_t len = strlen(aEntryName);
+ LOG(("ZipHandle::GetItem[%p] %s", this,
+ PromiseFlatCString(aEntryName).get()));
+ if (aEntryName.Length()) {
+ uint32_t len = aEntryName.Length();
//-- If the request is for a directory, make sure that synthetic entries
//-- are created for the directories without their own entry.
if (!mBuiltSynthetics) {
@@ -456,14 +457,14 @@ nsZipItem* nsZipArchive::GetItem(const char* aEntryName) {
}
}
MMAP_FAULT_HANDLER_BEGIN_HANDLE(mFd)
- nsZipItem* item = mFiles[HashName(aEntryName, len)];
+ nsZipItem* item = mFiles[HashName(aEntryName.BeginReading(), len)];
while (item) {
if ((len == item->nameLength) &&
- (!memcmp(aEntryName, item->Name(), len))) {
+ (!memcmp(aEntryName.BeginReading(), item->Name(), len))) {
// Successful GetItem() is a good indicator that the file is about to be
// read
if (mUseZipLog && mURI.Length()) {
- zipLog.Write(mURI, aEntryName);
+ zipLog.Write(mURI, aEntryName.BeginReading());
}
return item; //-- found it
}
@@ -1202,8 +1203,8 @@ uint8_t* nsZipCursor::ReadOrCopy(uint32_t* aBytesRead, bool aCopy) {
return buf;
}
-nsZipItemPtr_base::nsZipItemPtr_base(nsZipArchive* aZip, const char* aEntryName,
- bool doCRC)
+nsZipItemPtr_base::nsZipItemPtr_base(nsZipArchive* aZip,
+ const nsACString& aEntryName, bool doCRC)
: mReturnBuf(nullptr), mReadlen(0) {
// make sure the ziparchive hangs around
mZipHandle = aZip->GetFD();
diff --git a/modules/libjar/nsZipArchive.h b/modules/libjar/nsZipArchive.h
index 12e4b7ea002..c12c0d8f0df 100644
--- a/modules/libjar/nsZipArchive.h
+++ b/modules/libjar/nsZipArchive.h
@@ -116,14 +116,14 @@ class nsZipArchive final {
*
* @return status code
*/
- nsresult Test(const char* aEntryName);
+ nsresult Test(const nsACString& aEntryName);
/**
* GetItem
* @param aEntryName Name of file in the archive
* @return pointer to nsZipItem
*/
- nsZipItem* GetItem(const char* aEntryName);
+ nsZipItem* GetItem(const nsACString& aEntryName);
/**
* ExtractFile
@@ -303,7 +303,8 @@ class nsZipItemPtr_base {
* @param aEntryName Archive membername
* @param doCRC When set to true Read() will check crc
*/
- nsZipItemPtr_base(nsZipArchive* aZip, const char* aEntryName, bool doCRC);
+ nsZipItemPtr_base(nsZipArchive* aZip, const nsACString& aEntryName,
+ bool doCRC);
uint32_t Length() const { return mReadlen; }
@@ -321,7 +322,8 @@ class nsZipItemPtr final : public nsZipItemPtr_base {
" a number of assumptions.");
public:
- nsZipItemPtr(nsZipArchive* aZip, const char* aEntryName, bool doCRC = false)
+ nsZipItemPtr(nsZipArchive* aZip, const nsACString& aEntryName,
+ bool doCRC = false)
: nsZipItemPtr_base(aZip, aEntryName, doCRC) {}
/**
* @return buffer containing the whole zip member or nullptr on error.
@@ -363,7 +365,8 @@ class nsZipHandle final {
public:
static nsresult Init(nsIFile* file, nsZipHandle** ret,
PRFileDesc** aFd = nullptr);
- static nsresult Init(nsZipArchive* zip, const char* entry, nsZipHandle** ret);
+ static nsresult Init(nsZipArchive* zip, const nsACString& entry,
+ nsZipHandle** ret);
static nsresult Init(const uint8_t* aData, uint32_t aLen, nsZipHandle** aRet);
NS_METHOD_(MozExternalRefCountType) AddRef(void);
diff --git a/toolkit/mozapps/extensions/AddonManagerStartup.cpp b/toolkit/mozapps/extensions/AddonManagerStartup.cpp
index a2201cd6565..6a530c20d78 100644
--- a/toolkit/mozapps/extensions/AddonManagerStartup.cpp
+++ b/toolkit/mozapps/extensions/AddonManagerStartup.cpp
@@ -258,7 +258,7 @@ static Result<FileLocation, nsresult> GetFileLocation(nsIURI* uri) {
MOZ_TRY_VAR(file, GetFile(fileURI));
- location.Init(file, entry.get());
+ location.Init(file, entry);
}
return std::move(location);
diff --git a/xpcom/build/FileLocation.cpp b/xpcom/build/FileLocation.cpp
index 5162546f1d4..7e24af140e2 100644
--- a/xpcom/build/FileLocation.cpp
+++ b/xpcom/build/FileLocation.cpp
@@ -18,11 +18,11 @@ FileLocation::~FileLocation() = default;
FileLocation::FileLocation(nsIFile* aFile) { Init(aFile); }
-FileLocation::FileLocation(nsIFile* aFile, const char* aPath) {
+FileLocation::FileLocation(nsIFile* aFile, const nsACString& aPath) {
Init(aFile, aPath);
}
-FileLocation::FileLocation(nsZipArchive* aZip, const char* aPath) {
+FileLocation::FileLocation(nsZipArchive* aZip, const nsACString& aPath) {
Init(aZip, aPath);
}
@@ -37,14 +37,14 @@ FileLocation::FileLocation(FileLocation&& aOther)
aOther.mPath.Truncate();
}
-FileLocation::FileLocation(const FileLocation& aFile, const char* aPath) {
+FileLocation::FileLocation(const FileLocation& aFile, const nsACString& aPath) {
if (aFile.IsZip()) {
if (aFile.mBaseFile) {
- Init(aFile.mBaseFile, aFile.mPath.get());
+ Init(aFile.mBaseFile, aFile.mPath);
} else {
- Init(aFile.mBaseZip, aFile.mPath.get());
+ Init(aFile.mBaseZip, aFile.mPath);
}
- if (aPath) {
+ if (aPath.Length()) {
int32_t i = mPath.RFindChar('/');
if (kNotFound == i) {
mPath.Truncate(0);
@@ -54,7 +54,7 @@ FileLocation::FileLocation(const FileLocation& aFile, const char* aPath) {
mPath += aPath;
}
} else {
- if (aPath) {
+ if (aPath.Length()) {
nsCOMPtr<nsIFile> cfile;
aFile.mBaseFile->GetParent(getter_AddRefs(cfile));
@@ -69,7 +69,7 @@ FileLocation::FileLocation(const FileLocation& aFile, const char* aPath) {
}
cfile->AppendRelativeNativePath(pathStr);
#else
- cfile->AppendRelativeNativePath(nsDependentCString(aPath));
+ cfile->AppendRelativeNativePath(aPath);
#endif
Init(cfile);
} else {
@@ -84,13 +84,13 @@ void FileLocation::Init(nsIFile* aFile) {
mPath.Truncate();
}
-void FileLocation::Init(nsIFile* aFile, const char* aPath) {
+void FileLocation::Init(nsIFile* aFile, const nsACString& aPath) {
mBaseZip = nullptr;
mBaseFile = aFile;
mPath = aPath;
}
-void FileLocation::Init(nsZipArchive* aZip, const char* aPath) {
+void FileLocation::Init(nsZipArchive* aZip, const nsACString& aPath) {
mBaseZip = aZip;
mBaseFile = nullptr;
mPath = aPath;
@@ -158,7 +158,7 @@ nsresult FileLocation::GetData(Data& aData) {
aData.mZip = nsZipArchive::OpenArchive(mBaseFile);
}
if (aData.mZip) {
- aData.mItem = aData.mZip->GetItem(mPath.get());
+ aData.mItem = aData.mZip->GetItem(mPath);
if (aData.mItem) {
return NS_OK;
}
diff --git a/xpcom/build/FileLocation.h b/xpcom/build/FileLocation.h
index 0988a9595ff..3f6402bd8e3 100644
... (file diff truncated)