CVE-2026-11266
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TESTthird_party/zlib/contrib/tests/utils_unittest.cc |
modified |
Files Changed
third_party/zlib/contrib/minizip/unzip.cthird_party/zlib/contrib/minizip/unzip.hthird_party/zlib/contrib/tests/utils_unittest.cc
Patch
From 21217a08520feb424d7b7a61deacace304c68021 Mon Sep 17 00:00:00 2001 From: Hans Wennborg <[email protected]> Date: Mon, 04 May 2026 01:37:13 -0700 Subject: [PATCH] [minizip] Store the filename from Unicode Path Extra Field separately Instead of overriding unz_file_info64's size_filename and writing it to szFileName, store the Unicode filename and its size in new fields. Overriding size_filename could cause unzGoToNextFile() to jump to the wrong offset, potentially skipping a file in the central directory, as demonstrated by a newly added test. Storing the Unicode filename and its size separately avoids that problem and lets the caller decide what to do with the filename. Besides fixing the problem above, that seems like a safer way of bolting on support for this functionality. Bug: 40623474, 500521311 Change-Id: I06e6c8a8ff8baa729b10a67a998f8804803ae6cd Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7797172 Commit-Queue: Hans Wennborg <[email protected]> Reviewed-by: Joshua Pawlicki <[email protected]> Cr-Commit-Position: refs/heads/main@{#1624516} --- diff --git a/third_party/zlib/contrib/minizip/unzip.c b/third_party/zlib/contrib/minizip/unzip.c index 52bf191..0264f7a 100644 --- a/third_party/zlib/contrib/minizip/unzip.c +++ b/third_party/zlib/contrib/minizip/unzip.c @@ -859,6 +859,9 @@ uLong uL; uLong uFileNameCrc; + file_info.size_utf8_filename = 0; + file_info.utf8_filename[0] = '\0'; + if (file==NULL) return UNZ_PARAMERROR; s=(unz64_s*)file; @@ -1067,48 +1070,34 @@ } else { - uLong uCrc, fileNameSize; + uLong uCrc, utf8FileNameSize; if (unz64local_getLong(&s->z_filefunc, s->filestream, &uCrc) != UNZ_OK) { err = UNZ_ERRNO; } - fileNameSize = dataSize - (1 + 4); /* 1 for version, 4 for uCrc */ + utf8FileNameSize = dataSize - (1 + 4); /* 1 for version, 4 for uCrc */ /* Check CRC against file name in the header. */ if (uCrc != uFileNameCrc) { - if (ZSEEK64(s->z_filefunc, s->filestream, fileNameSize, ZLIB_FILEFUNC_SEEK_CUR) != 0) + if (ZSEEK64(s->z_filefunc, s->filestream, utf8FileNameSize, ZLIB_FILEFUNC_SEEK_CUR) != 0) { err = UNZ_ERRNO; } } else { - file_info.size_filename = fileNameSize; + file_info.size_utf8_filename = utf8FileNameSize; - char szCurrentFileName[UINT16_MAX] = {0}; - - if (file_info.size_filename > 0) + if (file_info.size_utf8_filename > 0) { - if (ZREAD64(s->z_filefunc, s->filestream, szCurrentFileName, file_info.size_filename) != file_info.size_filename) + if (ZREAD64(s->z_filefunc, s->filestream, file_info.utf8_filename, file_info.size_utf8_filename) != file_info.size_utf8_filename) { err = UNZ_ERRNO; } } - - if (szFileName != NULL) - { - if (fileNameBufferSize <= file_info.size_filename) - { - memcpy(szFileName, szCurrentFileName, fileNameBufferSize); - } - else - { - memcpy(szFileName, szCurrentFileName, file_info.size_filename); - szFileName[file_info.size_filename] = '\0'; - } - } + file_info.utf8_filename[file_info.size_utf8_filename] = '\0'; } } } @@ -1211,6 +1200,9 @@ pfile_info->compressed_size = (uLong)file_info64.compressed_size; pfile_info->uncompressed_size = (uLong)file_info64.uncompressed_size; + pfile_info->size_utf8_filename = file_info64.size_utf8_filename; + memcpy(pfile_info->utf8_filename, file_info64.utf8_filename, file_info64.size_utf8_filename + 1); + } return err; } diff --git a/third_party/zlib/contrib/minizip/unzip.h b/third_party/zlib/contrib/minizip/unzip.h index 9c98b608..05efa64e 100644 --- a/third_party/zlib/contrib/minizip/unzip.h +++ b/third_party/zlib/contrib/minizip/unzip.h @@ -42,6 +42,8 @@ #ifndef _unz64_H #define _unz64_H +#include <stdint.h> + #ifdef __cplusplus extern "C" { #endif @@ -128,6 +130,10 @@ uLong external_fa; /* external file attributes 4 bytes */ tm_unz tmu_date; + + /* Info-ZIP Unicode Path Extra Field */ + char utf8_filename[UINT16_MAX + 1]; /* UTF-8 Filename, null terminated */ + uLong size_utf8_filename; /* Length, excluding null terminator */ } unz_file_info64; typedef struct unz_file_info_s @@ -149,6 +155,10 @@ uLong external_fa; /* external file attributes 4 bytes */ tm_unz tmu_date; + + /* Info-ZIP Unicode Path Extra Field */ + char utf8_filename[UINT16_MAX + 1]; /* UTF-8 Filename, null terminated */ + uLong size_utf8_filename; /* Length, excluding null terminator */ } unz_file_info; extern int ZEXPORT unzStringFileNameCompare(const char* fileName1, diff --git a/third_party/zlib/contrib/tests/utils_unittest.cc b/third_party/zlib/contrib/tests/utils_unittest.cc index d1c2a97..f8cd93c7 100644 --- a/third_party/zlib/contrib/tests/utils_unittest.cc +++ b/third_party/zlib/contrib/tests/utils_unittest.cc @@ -1397,8 +1397,9 @@ ASSERT_EQ(unzGoToFirstFile(uzf), UNZ_OK); ASSERT_EQ(unzGetCurrentFileInfo(uzf, &file_info, long_buf, sizeof(long_buf), nullptr, 0, nullptr, 0), UNZ_OK); - ASSERT_EQ(file_info.size_filename, 14); - ASSERT_EQ(std::string(long_buf), "\xec\x83\x88 \xeb\xac\xb8\xec\x84\x9c.txt"); + ASSERT_EQ(file_info.size_filename, 11); + ASSERT_EQ(file_info.size_utf8_filename, 14); + ASSERT_EQ(std::string(file_info.utf8_filename), "\xec\x83\x88 \xeb\xac\xb8\xec\x84\x9c.txt"); // Even if the file name buffer is too short to hold the whole filename, the // unicode path extra field should get parsed correctly, size_filename set, @@ -1406,17 +1407,49 @@ ASSERT_EQ(unzGoToFirstFile(uzf), UNZ_OK); ASSERT_EQ(unzGetCurrentFileInfo(uzf, &file_info, short_buf, sizeof(short_buf), nullptr, 0, nullptr, 0), UNZ_OK); - ASSERT_EQ(file_info.size_filename, 14); - ASSERT_EQ(std::string(short_buf, sizeof(short_buf)), "\xec\x83\x88"); + ASSERT_EQ(file_info.size_filename, 11); + ASSERT_EQ(std::string(short_buf, sizeof(short_buf)), "\xc8\xfe\x20"); + ASSERT_EQ(file_info.size_utf8_filename, 14); + ASSERT_EQ(std::string(file_info.utf8_filename), "\xec\x83\x88 \xeb\xac\xb8\xec\x84\x9c.txt"); // Also with a null filename buffer, the unicode path extra field should get // parsed and size_filename set correctly. ASSERT_EQ(unzGoToFirstFile(uzf), UNZ_OK); ASSERT_EQ(unzGetCurrentFileInfo(uzf, &file_info, nullptr, 0, nullptr, 0, nullptr, 0), UNZ_OK); - ASSERT_EQ(file_info.size_filename, 14); + ASSERT_EQ(file_info.size_filename, 11); + ASSERT_EQ(file_info.size_utf8_filename, 14); + ASSERT_EQ(std::string(file_info.utf8_filename), "\xec\x83\x88 \xeb\xac\xb8\xec\x84\x9c.txt"); EXPECT_EQ(unzClose(uzf), UNZ_OK); } +TEST(ZlibTest, Crbug500521311) { + base::FilePath zip_file = TestDataDir().AppendASCII("bug500521311.zip"); + unzFile uzf = unzOpen(zip_file.AsUTF8Unsafe().c_str()); + ASSERT_NE(uzf, nullptr); + + char buf[256]; + unz_file_info file_info; + + // aaaaaa... + ASSERT_EQ(unzGoToFirstFile(uzf), UNZ_OK); + ASSERT_EQ(unzGetCurrentFileInfo(uzf, &file_info, buf, sizeof(buf), + nullptr, 0, nullptr, 0), UNZ_OK); + EXPECT_EQ(std::string(file_info.utf8_filename), + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + + // The Unicode Path Extra Field in the first Local File Header could + // previously cause unzGoToNextFile() to advance the wrong amount, + // potentially skipping a central directory entry. + ASSERT_EQ(unzGoToNextFile(uzf), UNZ_OK); +
Regression Test / PoC
diff --git a/third_party/zlib/contrib/tests/utils_unittest.cc b/third_party/zlib/contrib/tests/utils_unittest.cc
index d1c2a97..f8cd93c7 100644
--- a/third_party/zlib/contrib/tests/utils_unittest.cc
+++ b/third_party/zlib/contrib/tests/utils_unittest.cc
@@ -1397,8 +1397,9 @@
ASSERT_EQ(unzGoToFirstFile(uzf), UNZ_OK);
ASSERT_EQ(unzGetCurrentFileInfo(uzf, &file_info, long_buf, sizeof(long_buf),
nullptr, 0, nullptr, 0), UNZ_OK);
- ASSERT_EQ(file_info.size_filename, 14);
- ASSERT_EQ(std::string(long_buf), "\xec\x83\x88 \xeb\xac\xb8\xec\x84\x9c.txt");
+ ASSERT_EQ(file_info.size_filename, 11);
+ ASSERT_EQ(file_info.size_utf8_filename, 14);
+ ASSERT_EQ(std::string(file_info.utf8_filename), "\xec\x83\x88 \xeb\xac\xb8\xec\x84\x9c.txt");
// Even if the file name buffer is too short to hold the whole filename, the
// unicode path extra field should get parsed correctly, size_filename set,
@@ -1406,17 +1407,49 @@
ASSERT_EQ(unzGoToFirstFile(uzf), UNZ_OK);
ASSERT_EQ(unzGetCurrentFileInfo(uzf, &file_info, short_buf, sizeof(short_buf),
nullptr, 0, nullptr, 0), UNZ_OK);
- ASSERT_EQ(file_info.size_filename, 14);
- ASSERT_EQ(std::string(short_buf, sizeof(short_buf)), "\xec\x83\x88");
+ ASSERT_EQ(file_info.size_filename, 11);
+ ASSERT_EQ(std::string(short_buf, sizeof(short_buf)), "\xc8\xfe\x20");
+ ASSERT_EQ(file_info.size_utf8_filename, 14);
+ ASSERT_EQ(std::string(file_info.utf8_filename), "\xec\x83\x88 \xeb\xac\xb8\xec\x84\x9c.txt");
// Also with a null filename buffer, the unicode path extra field should get
// parsed and size_filename set correctly.
ASSERT_EQ(unzGoToFirstFile(uzf), UNZ_OK);
ASSERT_EQ(unzGetCurrentFileInfo(uzf, &file_info, nullptr, 0, nullptr, 0,
nullptr, 0), UNZ_OK);
- ASSERT_EQ(file_info.size_filename, 14);
+ ASSERT_EQ(file_info.size_filename, 11);
+ ASSERT_EQ(file_info.size_utf8_filename, 14);
+ ASSERT_EQ(std::string(file_info.utf8_filename), "\xec\x83\x88 \xeb\xac\xb8\xec\x84\x9c.txt");
EXPECT_EQ(unzClose(uzf), UNZ_OK);
}
+TEST(ZlibTest, Crbug500521311) {
+ base::FilePath zip_file = TestDataDir().AppendASCII("bug500521311.zip");
+ unzFile uzf = unzOpen(zip_file.AsUTF8Unsafe().c_str());
+ ASSERT_NE(uzf, nullptr);
+
+ char buf[256];
+ unz_file_info file_info;
+
+ // aaaaaa...
+ ASSERT_EQ(unzGoToFirstFile(uzf), UNZ_OK);
+ ASSERT_EQ(unzGetCurrentFileInfo(uzf, &file_info, buf, sizeof(buf),
+ nullptr, 0, nullptr, 0), UNZ_OK);
+ EXPECT_EQ(std::string(file_info.utf8_filename),
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
+
+ // The Unicode Path Extra Field in the first Local File Header could
+ // previously cause unzGoToNextFile() to advance the wrong amount,
+ // potentially skipping a central directory entry.
+ ASSERT_EQ(unzGoToNextFile(uzf), UNZ_OK);
+
+ // evil.exe
+ ASSERT_EQ(unzGetCurrentFileInfo(uzf, &file_info, buf, sizeof(buf),
+ nullptr, 0, nullptr, 0), UNZ_OK);
+ EXPECT_EQ(std::string(buf), "evil.exe");
+ EXPECT_EQ(unzGoToNextFile(uzf), UNZ_END_OF_LIST_OF_FILE);
+ EXPECT_EQ(unzClose(uzf), UNZ_OK);
+}
+
#endif
diff --git a/third_party/zlib/google/test/data/bug500521311.zip b/third_party/zlib/google/test/data/bug500521311.zip
new file mode 100644
index 0000000..cb55f30
--- /dev/null
+++ b/third_party/zlib/google/test/data/bug500521311.zip
Binary files differ
Original Bug Report
Safe Browsing bypass via ZIP Central Directory desync in minizip
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the security team.
Overview: A local Chromium patch to the minizip library incorrectly overwrites the physical filename size with the Unicode Path Extra Field length. This causes a Central Directory traversal desynchronization, allowing an attacker to craft a ZIP archive that hides malicious files from Safe Browsing scans while remaining extractable by standard tools.
Affected files:
third_party/zlib/contrib/minizip/unzip.cthird_party/zlib/google/zip_reader.ccchrome/utility/safe_browsing/zip_analyzer.cc
Estimated timestamp from git blame: 2025-05-20
Summary
A vulnerability in Chromium’s local modifications to the minizip library (third_party/zlib/contrib/minizip/unzip.c) allows for a ZIP central directory traversal desynchronization. This occurs when parsing the Info-ZIP Unicode Path Extra Field (header 0x7075). An attacker can exploit this to cause the ZIP parser to skip over malicious entries during analysis, leading to a Safe Browsing scan bypass while the hidden files remain extractable by standard archive tools like Windows Explorer or 7-Zip.
Vulnerability Details
The vulnerability stems from Chromium’s local patch 0016-minizip-parse-unicode-path-extra-field.patch. When unz64local_GetCurrentFileInfoInternal processes the Info-ZIP Unicode Path Extra Field, it incorrectly overwrites the physical filename length with the length of the UTF-8 path stored inside the extra field.
In third_party/zlib/contrib/minizip/unzip.c at line 1088:
file_info.size_filename = fileNameSize;
Where fileNameSize is derived from the extra field’s dataSize. This corrupted file_info.size_filename is then copied into the internal s->cur_file_info struct.
When iterating to the next file in the Central Directory (CD) via unzGoToNextFile, the corrupted size_filename value is used to calculate the physical start position of the next CD record:
s->pos_in_central_dir += SIZECENTRALDIRITEM + s->cur_file_info.size_filename +
s->cur_file_info.size_file_extra + s->cur_file_info.size_file_comment;
If an attacker crafts a ZIP entry where the Unicode path length specified in the extra field differs significantly from the actual physical filename length in the CD header, unzGoToNextFile will seek to an incorrect offset. By carefully padding the subsequent malicious entry, the attacker can cause the parser to overshoot the malicious entry entirely and land on a benign decoy entry.
Exploitation Scenario
The following steps describe how an attacker could potentially exploit this to bypass Safe Browsing:
- The attacker crafts a malicious ZIP archive containing three files: Entry A (benign), Entry B (malicious payload), and Entry C (benign).
- They modify the End of Central Directory (EOCD) record, setting
number_entryto2to hide the total count. - In Entry A’s Central Directory (CD) record, the physical
size_filenameis set to1byte. - An Info-ZIP Unicode Path Extra Field (0x7075) is added to Entry A’s CD record, with a
UnicodeNamelength of60bytes. - To pass minizip’s coherency check (
unz64local_CheckCurrentFileCoherencyHeader), the attacker crafts the Local File Header (LFH) for Entry A to havesize_filenameset to60bytes, and pads the physical LFH filename to 60 bytes. - Entry B’s CD record is padded (e.g., using the file comment field) so its total CD record size is exactly
59bytes (the difference between60and1). - During Safe Browsing analysis (
ZipAnalyzer::ResumeExtraction),unz64local_GetCurrentFileInfoInternalparses Entry A’s CD record and overwritessize_filenameto60. - Entry A successfully extracts because the LFH
size_filenamematches the corrupted CDsize_filename(both are60). - When advancing to the next file,
unzGoToNextFileadds60instead of1to the pointer. This59byte overshoot skips Entry B’s CD record perfectly, landing exactly on Entry C’s valid magic number. - Entry C is processed, and analysis terminates due to the
number_entrylimit of 2. Entry B is never scanned. - Standard extraction tools, which lack this custom Unicode Path patch, parse the CD correctly using the physical size of
1, exposing the hidden malware to the user.
(Note: These are suggested steps; our tooling agent does not have the ability to run code to produce a working proof-of-concept.)
Suggested Fix
Do not overwrite file_info.size_filename with the Unicode Extra Field length. The physical layout of the Central Directory relies on the physical filename length.
If the caller requires the Unicode filename, it should be passed via a separate field in unz_file_info64, or unzGoToNextFile must be refactored to use a separate internal variable that tracks the true physical length of the filename stored in the CD record, ensuring traversal calculations remain synchronized with the archive’s physical structure.
Evaluated with Chrome root at commit: 137d451a126685dd5010e6609db9f6d4a78d8234
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.