Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactPolicy bypass in SafeBrowsing
DescriptionPolicy bypass in SafeBrowsing
ComponentSafeBrowsing
Bug ClassLogic Error
Tracker500521311
Fix commit21217a08520f (chromium/src) +152/-54
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
TEST
third_party/zlib/contrib/tests/utils_unittest.cc
modified

Files Changed

  • third_party/zlib/contrib/minizip/unzip.c
  • third_party/zlib/contrib/minizip/unzip.h
  • third_party/zlib/contrib/tests/utils_unittest.cc
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);
+
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

reported by [email protected]

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.c
  • third_party/zlib/google/zip_reader.cc
  • chrome/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:

  1. The attacker crafts a malicious ZIP archive containing three files: Entry A (benign), Entry B (malicious payload), and Entry C (benign).
  2. They modify the End of Central Directory (EOCD) record, setting number_entry to 2 to hide the total count.
  3. In Entry A’s Central Directory (CD) record, the physical size_filename is set to 1 byte.
  4. An Info-ZIP Unicode Path Extra Field (0x7075) is added to Entry A’s CD record, with a UnicodeName length of 60 bytes.
  5. To pass minizip’s coherency check (unz64local_CheckCurrentFileCoherencyHeader), the attacker crafts the Local File Header (LFH) for Entry A to have size_filename set to 60 bytes, and pads the physical LFH filename to 60 bytes.
  6. Entry B’s CD record is padded (e.g., using the file comment field) so its total CD record size is exactly 59 bytes (the difference between 60 and 1).
  7. During Safe Browsing analysis (ZipAnalyzer::ResumeExtraction), unz64local_GetCurrentFileInfoInternal parses Entry A’s CD record and overwrites size_filename to 60.
  8. Entry A successfully extracts because the LFH size_filename matches the corrupted CD size_filename (both are 60).
  9. When advancing to the next file, unzGoToNextFile adds 60 instead of 1 to the pointer. This 59 byte overshoot skips Entry B’s CD record perfectly, landing exactly on Entry C’s valid magic number.
  10. Entry C is processed, and analysis terminates due to the number_entry limit of 2. Entry B is never scanned.
  11. 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.

View on issue tracker