CVE-2026-79151
Overview
Background
- Info-ZIP Unicode Path Extra Field
- an optional ZIP extra field (
up, ID0x7075) that carries a UTF-8 filename which some extractors prefer over the Central Directory filename, allowing an entry to present two different names. - `ZipReader::Entry`
- the per-entry metadata struct produced by
ZipReader, whosepathfield is the name consumers act on and whose newly addedphysical_pathfield records the Central Directory name. - `FileTypePolicies`
- the Safe Browsing service that classifies a filename as a checked binary (
IsCheckedBinaryFile) or a nested archive (IsArchiveFile) to decide whether an archived entry is inspected/reported. - `ZipAnalyzer`
- the utility-process component that walks a downloaded ZIP and calls
UpdateResultsForEntryto build the Safe BrowsingClientDownloadRequest.
Root Cause Analysis
Before the patch, ZipReader::OpenCurrentEntryInZip (the function enclosing the changed body in zip_reader.cc) decoded and normalized only a single name: when an Info-ZIP Unicode Path Extra Field was present, entry_.path_in_original_encoding was overwritten with the extra-field UTF-8 name and only that name reached entry_.path, discarding the Central Directory name entirely. ZipAnalyzer::AnalyzeZipFileImpl (the function enclosing the changed body in zip_analyzer.cc) then classified the entry using only entry->path, so a crafted archive could present a benign Unicode name (e.g. receipt.txt) while its Central Directory path was a checked binary or nested archive (e.g. malware.exe or payload.zip) — the name a native extractor such as Windows Explorer would actually use to write the file. The invariant violated is that Safe Browsing must classify an entry by the name under which it will be extracted on disk, but classification and extraction could disagree.
The fix decodes and normalizes the Central Directory name into a new entry_.physical_path and, in the analyzer, when the two names differ and the physical path is a checked binary or archive while the displayed path is not, uses the physical path for classification. Reporting still emits the extracted bytes once, but the entry is now inspected under the more dangerous name.
Attack Path
- Craft a dual-name entry
Build a ZIP whose entry has Central Directory filename
malware.exe(orpayload.zip) but an Info-ZIP Unicode Path Extra Field advertising a benign name likereceipt.txt, with a filename CRC that matches so the extra field is honored. - Deliver the archive
Get the victim to download the ZIP so Chrome’s download protection hands it to
ZipAnalyzerin the utility process. - Evade classification
Pre-patch,
ZipReaderexposes onlyreceipt.txt, soFileTypePoliciessees a harmless text file and the executable/archive entry is not reported as aZIPPED_EXECUTABLE/ZIPPED_ARCHIVE, bypassing Safe Browsing inspection. - Extract under the dangerous name
A native extractor that ignores the Unicode Path Extra Field writes
malware.exeto disk, so the user runs a binary that Safe Browsing never evaluated (inferred, based on the commit’s stated extractor divergence).
Impact Assessment
ZipAnalyzer). The precondition is that the victim downloads a crafted ZIP and opens it with an extractor that honors the Central Directory name; the advisory rates this improper input validation in Safe Browsing and does not report in-the-wild exploitation. Actual malware execution depends on the user extracting and running the entry (inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/utility/safe_browsing/zip_analyzer.cc |
modified | |
ifthird_party/zlib/google/zip_reader.cc |
modified | |
TEST_Fchrome/browser/safe_browsing/download_protection/file_analyzer_unittest.cc |
modified | |
TEST_Fthird_party/zlib/google/zip_reader_unittest.cc |
modified | |
FileWriterDelegateTestthird_party/zlib/google/zip_reader_unittest.cc |
modified |
Files Changed
third_party/zlib/google/zip_reader_unittest.ccthird_party/zlib/google/zip_reader.hthird_party/zlib/google/zip_reader.ccchrome/test/data/safe_browsing/download_protection/zipfile_unicode_path_extra_field.zipchrome/browser/safe_browsing/download_protection/file_analyzer_unittest.ccchrome/utility/safe_browsing/zip_analyzer.cc
Audit Directions
- Dual-name / metadata divergenceAudit any container parser (ZIP, TAR, MHTML, disk images) where an entry can present two names or two sizes, and confirm security classification uses the same name that extraction will use.
- Classify-vs-extract mismatchWherever Safe Browsing or policy code keys off a filename derived from attacker-controlled archive metadata, verify no later stage re-derives a different name from the same record.
- Extra-field trustReview handling of optional ZIP extra fields (Unicode Path, Unicode Comment, Zip64, NTFS) for cases where an optional field silently overrides a primary field without cross-checking CRCs or preserving the original.
Patch
From 4de51f777356fd3ec7fc127f176a35102ab75a6d Mon Sep 17 00:00:00 2001 From: Sergio Gonzalez Martin <[email protected]> Date: Tue, 21 Jul 2026 12:11:22 -0700 Subject: [PATCH] Use ZIP physical path for Safe Browsing checks Info-ZIP Unicode Path Extra Fields can make ZipReader expose a different name from the Central Directory path used by native extractors. Preserve both normalized names and have the ZIP analyzer consider both when classifying entries: it reports each extracted entry once, but uses the Central Directory path when that is the non-archive checked binary. Add reader and FileAnalyzer regression coverage. Bug: 517074167 Change-Id: If658273b92ef01334fce15bc53e146cac717ec55 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7881327 Reviewed-by: Adenilson Cavalcanti <[email protected]> Reviewed-by: Lily Chen <[email protected]> Commit-Queue: Sergio Gonzalez Martin <[email protected]> Reviewed-by: Varun Khaneja <[email protected]> Cr-Commit-Position: refs/heads/main@{#1665677} --- diff --git a/chrome/utility/safe_browsing/zip_analyzer.cc b/chrome/utility/safe_browsing/zip_analyzer.cc index 59735727..fb22b7f 100644 --- a/chrome/utility/safe_browsing/zip_analyzer.cc +++ b/chrome/utility/safe_browsing/zip_analyzer.cc @@ -26,6 +26,16 @@ namespace safe_browsing { +namespace { + +bool IsCheckedBinaryOrArchiveFile(const base::FilePath& path) { + const FileTypePolicies* file_type_policies = FileTypePolicies::GetInstance(); + return file_type_policies->IsCheckedBinaryFile(path) || + file_type_policies->IsArchiveFile(path); +} + +} // namespace + ZipAnalyzer::ZipAnalyzer() = default; ZipAnalyzer::~ZipAnalyzer() = default; @@ -68,8 +78,21 @@ EncryptionInfo::kKnownIncorrect; } - if (!UpdateResultsForEntry(temp_file_.Duplicate(), - GetRootPath().Append(entry->path), + // The Info-ZIP Unicode Path Extra Field can present a benign Unicode name + // (e.g. "receipt.txt") for an entry whose Central Directory path is a + // checked binary or nested archive (e.g. "malware.exe" or "payload.zip"). + // Different extractors may use either name, so consider both for Safe + // Browsing classification while reporting the extracted bytes once. + base::FilePath path = GetRootPath().Append(entry->path); + if (entry->path != entry->physical_path && !entry->is_directory) { + base::FilePath physical_path = GetRootPath().Append(entry->physical_path); + if (!IsCheckedBinaryOrArchiveFile(path) && + IsCheckedBinaryOrArchiveFile(physical_path)) { + path = std::move(physical_path); + } + } + + if (!UpdateResultsForEntry(temp_file_.Duplicate(), std::move(path), writer->file_length(), entry->is_encrypted, entry->is_directory, extract_success)) { return false; diff --git a/third_party/zlib/google/zip_reader.cc b/third_party/zlib/google/zip_reader.cc index 002a1e3..7b5b2896 100644 --- a/third_party/zlib/google/zip_reader.cc +++ b/third_party/zlib/google/zip_reader.cc @@ -16,6 +16,7 @@ #include "base/files/file.h" #include "base/files/file_util.h" #include "base/functional/bind.h" +#include "base/i18n/i18n_constants.h" #include "base/i18n/icu_string_conversions.h" #include "base/logging.h" #include "base/numerics/safe_conversions.h" @@ -307,25 +308,63 @@ DCHECK(path_in_zip[info.size_filename] == '\0'); entry_.path_in_original_encoding = path_in_zip.data(); + const char* const configured_encoding = + encoding_.empty() ? base::kCodepageUTF8 : encoding_.c_str(); + const char* entry_path_encoding = configured_encoding; + bool physical_path_is_directory = false; + bool physical_path_is_unsafe = false; + + // If an Info-ZIP Unicode Path Extra Field is present, the physical Central + // Directory path is about to be overridden. Decode and normalize it now into + // `entry_.physical_path` so consumers (e.g. Safe Browsing) can still see the + // name that other tools (e.g. Windows Explorer) would use for extraction. if (info.size_utf8_filename > 0) { + std::u16string physical_path_in_utf16; + if (!base::CodepageToUTF16(entry_.path_in_original_encoding, + configured_encoding, + base::OnStringConversionError::SUBSTITUTE, + &physical_path_in_utf16)) { + LOG(ERROR) << "Cannot convert path from encoding " << configured_encoding; + return false; + } + // Normalize() stores the normalized result in entry_.path; copy it before + // applying the Unicode Path Extra Field below. + Normalize(physical_path_in_utf16); + entry_.physical_path = entry_.path; + physical_path_is_directory = entry_.is_directory; + physical_path_is_unsafe = entry_.is_unsafe; + // Use the Info-ZIP Unicode Path Extra Field if present. DCHECK(info.utf8_filename[info.size_utf8_filename] == '\0'); entry_.path_in_original_encoding = info.utf8_filename; + entry_path_encoding = base::kCodepageUTF8; } // Convert path from original encoding to Unicode. std::u16string path_in_utf16; - const char* const encoding = encoding_.empty() ? "UTF-8" : encoding_.c_str(); - if (!base::CodepageToUTF16(entry_.path_in_original_encoding, encoding, + if (!base::CodepageToUTF16(entry_.path_in_original_encoding, + entry_path_encoding, base::OnStringConversionError::SUBSTITUTE, &path_in_utf16)) { - LOG(ERROR) << "Cannot convert path from encoding " << encoding; + LOG(ERROR) << "Cannot convert path from encoding " << entry_path_encoding; return false; } // Normalize path. Normalize(path_in_utf16); + if (info.size_utf8_filename > 0) { + // Treat an entry as a directory only if both names are directories; + // otherwise callers that analyze file entries should inspect it as a file. + entry_.is_directory = entry_.is_directory && physical_path_is_directory; + // Treat an entry as unsafe if either name is unsafe. + entry_.is_unsafe = entry_.is_unsafe || physical_path_is_unsafe; + } else { + // In the common case (no Unicode Path Extra Field) the physical path + // matches the effective path. + entry_.physical_path = entry_.path; + } + entry_.original_size = info.uncompressed_size; // The file content of this entry is encrypted if flag bit 0 is set. diff --git a/third_party/zlib/google/zip_reader.h b/third_party/zlib/google/zip_reader.h index 8bc4a21..af753614 100644 --- a/third_party/zlib/google/zip_reader.h +++ b/third_party/zlib/google/zip_reader.h @@ -130,6 +130,11 @@ // ./a -> DOT/a base::FilePath path; + // Physical path from the ZIP Central Directory, before applying the + // Info-ZIP Unicode Path Extra Field. This is converted and normalized in + // the same way as `path`. + base::FilePath physical_path; + // Size of the original uncompressed file, or 0 if the entry is a directory. // This value should not be trusted, because it is stored as metadata in the // ZIP archive and can be different from the real uncompressed size. diff --git a/chrome/browser/safe_browsing/download_protection/file_analyzer_unittest.cc b/chrome/browser/safe_browsing/download_protection/file_analyzer_unittest.cc index 6296843..2c4de7f 100644 --- a/chrome/browser/safe_browsing/download_protection/file_analyzer_unittest.cc +++ b/chrome/browser/safe_browsing/download_protection/file_analyzer_unittest.cc @@ -4,6 +4,10 @@ #include "chrome/browser/safe_browsing/download_protection/file_analyzer.h" +#include <stdint.h> + +#include <string> + #include "base/files/file_util.h" #include "base/files/scoped_temp_dir.h" #include "base/functional/bind.h" @@ -497,6 +501,83 @@ EXPECT_EQ(result_.inspection_performed, DownloadFileType::ZIP); } +TEST_F(FileAnalyzerTest, ArchivedBinariesUsesPhysicalZipPath) { + scoped_refptr<MockBinaryFeatureExtractor> extractor = + new testing::StrictMock<MockBinaryFeatureExtractor>(); + FileAnalyzer analyzer(extractor); + base::RunLoop run_loop; + + base::FilePath target_file_name(FILE_PATH_LITERAL("target.zip")); + base::FilePath tmp_path = + temp_dir_.GetPath().Append(FILE_PATH_LITERAL("tmp.crdownload")); + // Regression fixture for crbug.com/517074167. The ZIP's physical path is an + // executable, but its Info-ZIP Unicode Path Extra Field presents a benign + // filename. The fixture's filename CRC matches the physical path, so the + // Unicode Path Extra Field is honored. + base::FilePath zip_path; + ASSERT_TRUE(base::PathService::Get(chrome::DIR_TEST_DATA, &zip_path)); + zip_path = zip_path.AppendASCII("safe_browsing") + .AppendASCII("download_protection") + .AppendASCII("zipfile_unicode_path_extra_field.zip"); + ASSERT_TRUE(base::CopyFile(zip_path, tmp_path)); + + analyzer.Start( + target_file_name, tmp_path, /*password=*/std::nullopt, + base::BindOnce(&FileAnalyzerTest::DoneCallback, base::Unretained(this), + run_loop.QuitClosure())); + run_loop.Run(); + + ASSERT_TRUE(has_result_); + EXPECT_EQ(result_.type, ClientDownloadRequest::ZIPPED_EXECUTABLE); + EXPECT_EQ(result_.inspection_performed, DownloadFileType::ZIP); + ASSERT_EQ(result_.archived_binaries.size(), 1); + EXPECT_TRUE(result_.archived_binaries[0].is_executable()); + EXPECT_EQ(result_.archived_binaries[0].file_path(), "malware.exe"); +} + +TEST_F(FileAnalyzerTest, ArchivedArchiveUsesPhysicalZipPath) { + scoped_refptr<MockBinaryFeatureExtractor> extractor = + new testing::StrictMock<MockBinaryFeatureExtractor>(); + FileAnalyzer analyzer(extractor); + base::RunLoop run_loop; + + base::FilePath target_file_name(FILE_PATH_LITERAL("target.zip")); + base::FilePath tmp_path = + temp_dir_.GetPath().Append(FILE_PATH_LITERAL("tmp.crdownload")); + // Regression fixture for crbug.com/517074167. The ZIP's physical path is a + // nested archive, but its Info-ZIP Unicode Path Extra Field presents a benign + // filename. The fixture's filename CRC matches the physical path, so the + // Unicode Path Extra Field is honored. + static constexpr uint8_t zip_data[] = { + 0x50, 0x4b, 0x03, 0x04, 0x0a, 0x03, 0x00, 0x00, 0x00, 0x00, 0xd0, 0x71, + 0x91, 0x4e, 0x11, 0x2c, 0xf9, 0x51, 0x09, 0x00, 0x00, 0x00, 0x09, 0x00, + 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, + 0x64, 0x2e, 0x7a, 0x69, 0x70, 0x54, 0x65, 0x73, 0x74, 0x20, 0x64, 0x61, + 0x74, 0x61, 0x50, 0x4b, 0x01, 0x02, 0x3f, 0x03, 0x0a, 0x03, 0x00, 0x00, + 0x00, 0x00, 0xd0, 0x71, 0x91, 0x4e, 0x11, 0x2c, 0xf9, 0x51, 0x09, 0x00, + 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x14, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x20, 0x80, 0xc9, 0x81, 0x00, 0x00, 0x00, 0x00, + 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x2e, 0x7a, 0x69, 0x70, 0x75, + 0x70, 0x10, 0x00, 0x01, 0x0c, 0xe9, 0x91, 0x5f, 0x72, 0x65, 0x63, 0x65, + 0x69, 0x70, 0x74, 0x2e, 0x74, 0x78, 0x74, 0x50, 0x4b, 0x05, 0x06, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x4d, 0x00, 0x00, 0x00, 0x32, + 0x00, 0x00, 0x00, 0x00, 0x00}; + ASSERT_TRUE(base::WriteFile(tmp_path, base::span(zip_data))); + + analyzer.Start( + target_file_name, tmp_path, /*password=*/std::nullopt, + base::BindOnce(&FileAnalyzerTest::DoneCallback, base::Unretained(this), + run_loop.QuitClosure())); + run_loop.Run(); + + ASSERT_TRUE(has_result_); + EXPECT_EQ(result_.type, ClientDownloadRequest::ZIPPED_ARCHIVE); + EXPECT_TRUE(result_.archived_archive); + ASSERT_EQ(result_.archived_binaries.size(), 1); + EXPECT_TRUE(result_.archived_binaries[0].is_archive()); + EXPECT_EQ(result_.archived_binaries[0].file_path(), "payload.zip"); +} + TEST_F(FileAnalyzerTest, ArchivedBinariesSkipsSafeFiles) { scoped_refptr<MockBinaryFeatureExtractor> extractor = new testing::StrictMock<MockBinaryFeatureExtractor>(); diff --git a/chrome/test/data/safe_browsing/download_protection/zipfile_unicode_path_extra_field.zip b/chrome/test/data/safe_browsing/download_protection/zipfile_unicode_path_extra_field.zip new file mode 100644 index 0000000..0ff4b5f --- /dev/null +++ b/chrome/test/data/safe_browsing/download_protection/zipfile_unicode_path_extra_field.zip Binary files differ diff --git a/third_party/zlib/google/zip_reader_unittest.cc b/third_party/zlib/google/zip_reader_unittest.cc index 3de6568..578539f 100644 --- a/third_party/zlib/google/zip_reader_unittest.cc +++ b/third_party/zlib/google/zip_reader_unittest.cc @@ -998,6 +998,123 @@ reader.Next(); } +TEST_F(ZipReaderTest, UnicodePathExtraFieldPreservesPhysicalPath) { + static const char test_data[] = { + 0x50, 0x4b, 0x03, 0x04, 0x0a, 0x03, 0x00, 0x00, 0x00, 0x00, 0xd0, 0x71, + 0x91, 0x4e, 0x11, 0x2c, 0xf9, 0x51, 0x09, 0x00, 0x00, 0x00, 0x09, 0x00, + 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x6d, 0x61, 0x6c, 0x77, 0x61, 0x72, + 0x65, 0x2e, 0x65, 0x78, 0x65, 0x54, 0x65, 0x73, 0x74, 0x20, 0x64, 0x61, + 0x74, 0x61, 0x50, 0x4b, 0x01, 0x02, 0x3f, 0x03, 0x0a, 0x03, 0x00, 0x00, + 0x00, 0x00, 0xd0, 0x71, 0x91, 0x4e, 0x11, 0x2c, 0xf9, 0x51, 0x09, 0x00, + 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x15, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x20, 0x80, 0xc9, 0x81, 0x00, 0x00, 0x00, 0x00, + 0x6d, 0x61, 0x6c, 0x77, 0x61, 0x72, 0x65, 0x2e, 0x65, 0x78, 0x65, 0x75, + 0x70, 0x11, 0x00, 0x01, 0xed, 0x4b, 0x16, 0x3c, 0x64, 0x6f, 0x77, 0x6e, + 0x6c, 0x6f, 0x61, 0x64, 0x2e, 0x74, 0x78, 0x74, 0x50, 0x4b, 0x05, 0x06, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x4e, 0x00, 0x00, 0x00, + 0x32, 0x00, 0x00, 0x00, 0x00, 0x00}; + + std::string test_string(test_data, sizeof(test_data)); + ZipReader reader; + ASSERT_TRUE(reader.OpenFromString(test_string)); + const ZipReader::Entry* entry = reader.Next(); + ASSERT_TRUE(entry); + // The Unicode Path Extra Field overrides the Central Directory filename, + // but the original physical path is preserved separately. `is_unsafe` tracks + // path traversal safety, not whether the filename looks executable. + EXPECT_EQ(base::FilePath::FromUTF8Unsafe("download.txt"), entry->path); + EXPECT_EQ(base::FilePath::FromUTF8Unsafe("malware.exe"), + entry->physical_path); + EXPECT_FALSE(entry->is_directory); + EXPECT_FALSE(entry->is_unsafe); +} + +TEST_F(ZipReaderTest, UnicodePathExtraFieldUsesUtf8WithConfiguredEncoding) { + static constexpr uint8_t test_data[] = { + 0x50, 0x4b, 0x03, 0x04, 0x0a, 0x03, 0x00, 0x00, 0x00, 0x00, 0xd0, 0x71, + 0x91, 0x4e, 0x11, 0x2c, 0xf9, 0x51, 0x09, 0x00, 0x00, 0x00, 0x09, 0x00, + 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x6d, 0x61, 0x6c, 0x77, 0x61, 0x72, + 0x65, 0x2e, 0x65, 0x78, 0x65, 0x54, 0x65, 0x73, 0x74, 0x20, 0x64, 0x61, + 0x74, 0x61, 0x50, 0x4b, 0x01, 0x02, 0x3f, 0x03, 0x0a, 0x03, 0x00, 0x00, + 0x00, 0x00, 0xd0, 0x71, 0x91, 0x4e, 0x11, 0x2c, 0xf9, 0x51, 0x09, 0x00, + 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x15, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x20, 0x80, 0xc9, 0x81, 0x00, 0x00, 0x00, 0x00, + 0x6d, 0x61, 0x6c, 0x77, 0x61, 0x72, 0x65, 0x2e, 0x65, 0x78, 0x65, 0x75, + 0x70, 0x11, 0x00, 0x01, 0xed, 0x4b, 0x16, 0x3c, 0x72, 0xc3, 0xa9, 0x73, + 0x75, 0x6d, 0xc3, 0xa9, 0x2e, 0x74, 0x78, 0x74, 0x50, 0x4b, 0x05, 0x06, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x4e, 0x00, 0x00, 0x00, + 0x32, 0x00, 0x00, 0x00, 0x00, 0x00}; + + std::string test_string(reinterpret_cast<const char*>(test_data), + sizeof(test_data)); + ZipReader reader; + ASSERT_TRUE(reader.OpenFromString(test_string)); + reader.SetEncoding("windows-1252"); + const ZipReader::Entry* entry = reader.Next(); + ASSERT_TRUE(entry); + EXPECT_EQ(base::FilePath::FromUTF8Unsafe("résumé.txt"), entry->path); + EXPECT_EQ(base::FilePath::FromUTF8Unsafe("malware.exe"), + entry->physical_path); +} + +TEST_F(ZipReaderTest, UnicodePathExtraFieldFileIfEitherPathIsFile) { + static const char test_data[] = { + 0x50, 0x4b, 0x03, 0x04, 0x0a, 0x03, 0x00, 0x00, 0x00, 0x00, 0xd0, 0x71, + 0x91, 0x4e, 0x11, 0x2c, 0xf9, 0x51, 0x09, 0x00, 0x00, 0x00, 0x09, 0x00, + 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x6d, 0x61, 0x6c, 0x77, 0x61, 0x72, + 0x65, 0x2e, 0x65, 0x78, 0x65, 0x2f, 0x54, 0x65, 0x73, 0x74, 0x20, 0x64, + 0x61, 0x74, 0x61, 0x50, 0x4b, 0x01, 0x02, 0x3f, 0x03, 0x0a, 0x03, 0x00, + 0x00, 0x00, 0x00, 0xd0, 0x71, 0x91, 0x4e, 0x11, 0x2c, 0xf9, 0x51, 0x09, + 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x14, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x80, 0xc9, 0x81, 0x00, 0x00, 0x00, + 0x00, 0x6d, 0x61, 0x6c, 0x77, 0x61, 0x72, 0x65, 0x2e, 0x65, 0x78, 0x65, + 0x2f, 0x75, 0x70, 0x10, 0x00, 0x01, 0x5a, 0x5a, 0x54, 0xa7, 0x6d, 0x61, + 0x6c, 0x77, 0x61, 0x72, 0x65, 0x2e, 0x65, 0x78, 0x65, 0x50, 0x4b, 0x05, + 0x06, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x4e, 0x00, 0x00, + 0x00, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00}; + + std::string test_string(test_data, sizeof(test_data)); + ZipReader reader; + ASSERT_TRUE(reader.OpenFromString(test_string)); + const ZipReader::Entry* entry = reader.Next(); + ASSERT_TRUE(entry); + EXPECT_EQ(base::FilePath::FromUTF8Unsafe("malware.exe"), entry->path); + EXPECT_EQ(base::FilePath::FromUTF8Unsafe("malware.exe/"), + entry->physical_path); + EXPECT_FALSE(entry->is_directory); + EXPECT_FALSE(entry->is_unsafe); +} + +TEST_F(ZipReaderTest, UnicodePathExtraFieldPreservesUnsafePhysicalPath) { + static const char test_data[] = { + 0x50, 0x4b, 0x03, 0x04, 0x0a, 0x03, 0x00, 0x00, 0x00, 0x00, 0xd0, 0x71, + 0x91, 0x4e, 0x11, 0x2c, 0xf9, 0x51, 0x09, 0x00, 0x00, 0x00, 0x09, 0x00, + 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x6d, 0x61, 0x6c, + 0x77, 0x61, 0x72, 0x65, 0x2e, 0x65, 0x78, 0x65, 0x2f, + 0x2e, 0x2e, 0x54, 0x65, 0x73, 0x74, + 0x20, 0x64, 0x61, 0x74, 0x61, 0x50, 0x4b, 0x01, 0x02, 0x3f, 0x03, 0x0a, + 0x03, 0x00, 0x00, 0x00, 0x00, 0xd0, 0x71, 0x91, 0x4e, 0x11, 0x2c, 0xf9, + 0x51, 0x09, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x15, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x80, 0xc9, 0x81, 0x00, + 0x00, 0x00, 0x00, 0x6d, 0x61, 0x6c, 0x77, 0x61, 0x72, + 0x65, 0x2e, 0x65, 0x78, 0x65, 0x2f, 0x2e, 0x2e, 0x75, + 0x70, 0x11, 0x00, 0x01, 0x7c, 0xbc, 0xe2, 0x5d, 0x64, + 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x2e, 0x74, + 0x78, 0x74, 0x50, 0x4b, 0x05, 0x06, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x01, 0x00, 0x51, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, 0x00, 0x00}; + + std::string test_string(test_data, sizeof(test_data)); + ZipReader reader; + ASSERT_TRUE(reader.OpenFromString(test_string)); + const ZipReader::Entry* entry = reader.Next(); + ASSERT_TRUE(entry); + EXPECT_EQ(base::FilePath::FromUTF8Unsafe("download.txt"), entry->path); + EXPECT_EQ(base::FilePath::FromUTF8Unsafe("malware.exe/UP"), + entry->physical_path); + EXPECT_FALSE(entry->is_directory); + EXPECT_TRUE(entry->is_unsafe); +} + class FileWriterDelegateTest : public ::testing::Test { protected: void SetUp() override {
Regression Test / PoC
diff --git a/chrome/browser/safe_browsing/download_protection/file_analyzer_unittest.cc b/chrome/browser/safe_browsing/download_protection/file_analyzer_unittest.cc
index 6296843..2c4de7f 100644
--- a/chrome/browser/safe_browsing/download_protection/file_analyzer_unittest.cc
+++ b/chrome/browser/safe_browsing/download_protection/file_analyzer_unittest.cc
@@ -4,6 +4,10 @@
#include "chrome/browser/safe_browsing/download_protection/file_analyzer.h"
+#include <stdint.h>
+
+#include <string>
+
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/bind.h"
@@ -497,6 +501,83 @@
EXPECT_EQ(result_.inspection_performed, DownloadFileType::ZIP);
}
+TEST_F(FileAnalyzerTest, ArchivedBinariesUsesPhysicalZipPath) {
+ scoped_refptr<MockBinaryFeatureExtractor> extractor =
+ new testing::StrictMock<MockBinaryFeatureExtractor>();
+ FileAnalyzer analyzer(extractor);
+ base::RunLoop run_loop;
+
+ base::FilePath target_file_name(FILE_PATH_LITERAL("target.zip"));
+ base::FilePath tmp_path =
+ temp_dir_.GetPath().Append(FILE_PATH_LITERAL("tmp.crdownload"));
+ // Regression fixture for crbug.com/517074167. The ZIP's physical path is an
+ // executable, but its Info-ZIP Unicode Path Extra Field presents a benign
+ // filename. The fixture's filename CRC matches the physical path, so the
+ // Unicode Path Extra Field is honored.
+ base::FilePath zip_path;
+ ASSERT_TRUE(base::PathService::Get(chrome::DIR_TEST_DATA, &zip_path));
+ zip_path = zip_path.AppendASCII("safe_browsing")
+ .AppendASCII("download_protection")
+ .AppendASCII("zipfile_unicode_path_extra_field.zip");
+ ASSERT_TRUE(base::CopyFile(zip_path, tmp_path));
+
+ analyzer.Start(
+ target_file_name, tmp_path, /*password=*/std::nullopt,
+ base::BindOnce(&FileAnalyzerTest::DoneCallback, base::Unretained(this),
+ run_loop.QuitClosure()));
+ run_loop.Run();
+
+ ASSERT_TRUE(has_result_);
+ EXPECT_EQ(result_.type, ClientDownloadRequest::ZIPPED_EXECUTABLE);
+ EXPECT_EQ(result_.inspection_performed, DownloadFileType::ZIP);
+ ASSERT_EQ(result_.archived_binaries.size(), 1);
+ EXPECT_TRUE(result_.archived_binaries[0].is_executable());
+ EXPECT_EQ(result_.archived_binaries[0].file_path(), "malware.exe");
+}
+
+TEST_F(FileAnalyzerTest, ArchivedArchiveUsesPhysicalZipPath) {
+ scoped_refptr<MockBinaryFeatureExtractor> extractor =
+ new testing::StrictMock<MockBinaryFeatureExtractor>();
+ FileAnalyzer analyzer(extractor);
+ base::RunLoop run_loop;
+
+ base::FilePath target_file_name(FILE_PATH_LITERAL("target.zip"));
+ base::FilePath tmp_path =
+ temp_dir_.GetPath().Append(FILE_PATH_LITERAL("tmp.crdownload"));
+ // Regression fixture for crbug.com/517074167. The ZIP's physical path is a
+ // nested archive, but its Info-ZIP Unicode Path Extra Field presents a benign
+ // filename. The fixture's filename CRC matches the physical path, so the
+ // Unicode Path Extra Field is honored.
+ static constexpr uint8_t zip_data[] = {
+ 0x50, 0x4b, 0x03, 0x04, 0x0a, 0x03, 0x00, 0x00, 0x00, 0x00, 0xd0, 0x71,
+ 0x91, 0x4e, 0x11, 0x2c, 0xf9, 0x51, 0x09, 0x00, 0x00, 0x00, 0x09, 0x00,
+ 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61,
+ 0x64, 0x2e, 0x7a, 0x69, 0x70, 0x54, 0x65, 0x73, 0x74, 0x20, 0x64, 0x61,
+ 0x74, 0x61, 0x50, 0x4b, 0x01, 0x02, 0x3f, 0x03, 0x0a, 0x03, 0x00, 0x00,
+ 0x00, 0x00, 0xd0, 0x71, 0x91, 0x4e, 0x11, 0x2c, 0xf9, 0x51, 0x09, 0x00,
+ 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x14, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x20, 0x80, 0xc9, 0x81, 0x00, 0x00, 0x00, 0x00,
+ 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x2e, 0x7a, 0x69, 0x70, 0x75,
+ 0x70, 0x10, 0x00, 0x01, 0x0c, 0xe9, 0x91, 0x5f, 0x72, 0x65, 0x63, 0x65,
+ 0x69, 0x70, 0x74, 0x2e, 0x74, 0x78, 0x74, 0x50, 0x4b, 0x05, 0x06, 0x00,
+ 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x4d, 0x00, 0x00, 0x00, 0x32,
+ 0x00, 0x00, 0x00, 0x00, 0x00};
+ ASSERT_TRUE(base::WriteFile(tmp_path, base::span(zip_data)));
+
+ analyzer.Start(
+ target_file_name, tmp_path, /*password=*/std::nullopt,
+ base::BindOnce(&FileAnalyzerTest::DoneCallback, base::Unretained(this),
+ run_loop.QuitClosure()));
+ run_loop.Run();
+
+ ASSERT_TRUE(has_result_);
+ EXPECT_EQ(result_.type, ClientDownloadRequest::ZIPPED_ARCHIVE);
+ EXPECT_TRUE(result_.archived_archive);
+ ASSERT_EQ(result_.archived_binaries.size(), 1);
+ EXPECT_TRUE(result_.archived_binaries[0].is_archive());
+ EXPECT_EQ(result_.archived_binaries[0].file_path(), "payload.zip");
+}
+
TEST_F(FileAnalyzerTest, ArchivedBinariesSkipsSafeFiles) {
scoped_refptr<MockBinaryFeatureExtractor> extractor =
new testing::StrictMock<MockBinaryFeatureExtractor>();
diff --git a/chrome/test/data/safe_browsing/download_protection/zipfile_unicode_path_extra_field.zip b/chrome/test/data/safe_browsing/download_protection/zipfile_unicode_path_extra_field.zip
new file mode 100644
index 0000000..0ff4b5f
--- /dev/null
+++ b/chrome/test/data/safe_browsing/download_protection/zipfile_unicode_path_extra_field.zip
Binary files differ
diff --git a/third_party/zlib/google/zip_reader_unittest.cc b/third_party/zlib/google/zip_reader_unittest.cc
index 3de6568..578539f 100644
--- a/third_party/zlib/google/zip_reader_unittest.cc
+++ b/third_party/zlib/google/zip_reader_unittest.cc
@@ -998,6 +998,123 @@
reader.Next();
}
+TEST_F(ZipReaderTest, UnicodePathExtraFieldPreservesPhysicalPath) {
+ static const char test_data[] = {
+ 0x50, 0x4b, 0x03, 0x04, 0x0a, 0x03, 0x00, 0x00, 0x00, 0x00, 0xd0, 0x71,
+ 0x91, 0x4e, 0x11, 0x2c, 0xf9, 0x51, 0x09, 0x00, 0x00, 0x00, 0x09, 0x00,
+ 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x6d, 0x61, 0x6c, 0x77, 0x61, 0x72,
+ 0x65, 0x2e, 0x65, 0x78, 0x65, 0x54, 0x65, 0x73, 0x74, 0x20, 0x64, 0x61,
+ 0x74, 0x61, 0x50, 0x4b, 0x01, 0x02, 0x3f, 0x03, 0x0a, 0x03, 0x00, 0x00,
+ 0x00, 0x00, 0xd0, 0x71, 0x91, 0x4e, 0x11, 0x2c, 0xf9, 0x51, 0x09, 0x00,
+ 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x15, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x20, 0x80, 0xc9, 0x81, 0x00, 0x00, 0x00, 0x00,
+ 0x6d, 0x61, 0x6c, 0x77, 0x61, 0x72, 0x65, 0x2e, 0x65, 0x78, 0x65, 0x75,
+ 0x70, 0x11, 0x00, 0x01, 0xed, 0x4b, 0x16, 0x3c, 0x64, 0x6f, 0x77, 0x6e,
+ 0x6c, 0x6f, 0x61, 0x64, 0x2e, 0x74, 0x78, 0x74, 0x50, 0x4b, 0x05, 0x06,
+ 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x4e, 0x00, 0x00, 0x00,
+ 0x32, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+ std::string test_string(test_data, sizeof(test_data));
+ ZipReader reader;
+ ASSERT_TRUE(reader.OpenFromString(test_string));
+ const ZipReader::Entry* entry = reader.Next();
+ ASSERT_TRUE(entry);
+ // The Unicode Path Extra Field overrides the Central Directory filename,
+ // but the original physical path is preserved separately. `is_unsafe` tracks
+ // path traversal safety, not whether the filename looks executable.
+ EXPECT_EQ(base::FilePath::FromUTF8Unsafe("download.txt"), entry->path);
+ EXPECT_EQ(base::FilePath::FromUTF8Unsafe("malware.exe"),
+ entry->physical_path);
+ EXPECT_FALSE(entry->is_directory);
+ EXPECT_FALSE(entry->is_unsafe);
+}
+
+TEST_F(ZipReaderTest, UnicodePathExtraFieldUsesUtf8WithConfiguredEncoding) {
+ static constexpr uint8_t test_data[] = {
+ 0x50, 0x4b, 0x03, 0x04, 0x0a, 0x03, 0x00, 0x00, 0x00, 0x00, 0xd0, 0x71,
+ 0x91, 0x4e, 0x11, 0x2c, 0xf9, 0x51, 0x09, 0x00, 0x00, 0x00, 0x09, 0x00,
+ 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x6d, 0x61, 0x6c, 0x77, 0x61, 0x72,
+ 0x65, 0x2e, 0x65, 0x78, 0x65, 0x54, 0x65, 0x73, 0x74, 0x20, 0x64, 0x61,
+ 0x74, 0x61, 0x50, 0x4b, 0x01, 0x02, 0x3f, 0x03, 0x0a, 0x03, 0x00, 0x00,
+ 0x00, 0x00, 0xd0, 0x71, 0x91, 0x4e, 0x11, 0x2c, 0xf9, 0x51, 0x09, 0x00,
+ 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x15, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x20, 0x80, 0xc9, 0x81, 0x00, 0x00, 0x00, 0x00,
+ 0x6d, 0x61, 0x6c, 0x77, 0x61, 0x72, 0x65, 0x2e, 0x65, 0x78, 0x65, 0x75,
+ 0x70, 0x11, 0x00, 0x01, 0xed, 0x4b, 0x16, 0x3c, 0x72, 0xc3, 0xa9, 0x73,
+ 0x75, 0x6d, 0xc3, 0xa9, 0x2e, 0x74, 0x78, 0x74, 0x50, 0x4b, 0x05, 0x06,
+ 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x4e, 0x00, 0x00, 0x00,
+ 0x32, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+ std::string test_string(reinterpret_cast<const char*>(test_data),
+ sizeof(test_data));
+ ZipReader reader;
+ ASSERT_TRUE(reader.OpenFromString(test_string));
+ reader.SetEncoding("windows-1252");
+ const ZipReader::Entry* entry = reader.Next();
+ ASSERT_TRUE(entry);
+ EXPECT_EQ(base::FilePath::FromUTF8Unsafe("résumé.txt"), entry->path);
+ EXPECT_EQ(base::FilePath::FromUTF8Unsafe("malware.exe"),
+ entry->physical_path);
+}
+
+TEST_F(ZipReaderTest, UnicodePathExtraFieldFileIfEitherPathIsFile) {
+ static const char test_data[] = {
+ 0x50, 0x4b, 0x03, 0x04, 0x0a, 0x03, 0x00, 0x00, 0x00, 0x00, 0xd0, 0x71,
+ 0x91, 0x4e, 0x11, 0x2c, 0xf9, 0x51, 0x09, 0x00, 0x00, 0x00, 0x09, 0x00,
+ 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x6d, 0x61, 0x6c, 0x77, 0x61, 0x72,
+ 0x65, 0x2e, 0x65, 0x78, 0x65, 0x2f, 0x54, 0x65, 0x73, 0x74, 0x20, 0x64,
+ 0x61, 0x74, 0x61, 0x50, 0x4b, 0x01, 0x02, 0x3f, 0x03, 0x0a, 0x03, 0x00,
+ 0x00, 0x00, 0x00, 0xd0, 0x71, 0x91, 0x4e, 0x11, 0x2c, 0xf9, 0x51, 0x09,
+ 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x14, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x80, 0xc9, 0x81, 0x00, 0x00, 0x00,
+ 0x00, 0x6d, 0x61, 0x6c, 0x77, 0x61, 0x72, 0x65, 0x2e, 0x65, 0x78, 0x65,
+ 0x2f, 0x75, 0x70, 0x10, 0x00, 0x01, 0x5a, 0x5a, 0x54, 0xa7, 0x6d, 0x61,
+ 0x6c, 0x77, 0x61, 0x72, 0x65, 0x2e, 0x65, 0x78, 0x65, 0x50, 0x4b, 0x05,
+ 0x06, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x4e, 0x00, 0x00,
+ 0x00, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+ std::string test_string(test_data, sizeof(test_data));
+ ZipReader reader;
+ ASSERT_TRUE(reader.OpenFromString(test_string));
+ const ZipReader::Entry* entry = reader.Next();
+ ASSERT_TRUE(entry);
+ EXPECT_EQ(base::FilePath::FromUTF8Unsafe("malware.exe"), entry->path);
+ EXPECT_EQ(base::FilePath::FromUTF8Unsafe("malware.exe/"),
+ entry->physical_path);
+ EXPECT_FALSE(entry->is_directory);
+ EXPECT_FALSE(entry->is_unsafe);
+}
+
+TEST_F(ZipReaderTest, UnicodePathExtraFieldPreservesUnsafePhysicalPath) {
+ static const char test_data[] = {
+ 0x50, 0x4b, 0x03, 0x04, 0x0a, 0x03, 0x00, 0x00, 0x00, 0x00, 0xd0, 0x71,
+ 0x91, 0x4e, 0x11, 0x2c, 0xf9, 0x51, 0x09, 0x00, 0x00, 0x00, 0x09, 0x00,
+ 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x6d, 0x61, 0x6c,
+ 0x77, 0x61, 0x72, 0x65, 0x2e, 0x65, 0x78, 0x65, 0x2f,
+ 0x2e, 0x2e, 0x54, 0x65, 0x73, 0x74,
+ 0x20, 0x64, 0x61, 0x74, 0x61, 0x50, 0x4b, 0x01, 0x02, 0x3f, 0x03, 0x0a,
+ 0x03, 0x00, 0x00, 0x00, 0x00, 0xd0, 0x71, 0x91, 0x4e, 0x11, 0x2c, 0xf9,
+ 0x51, 0x09, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x15,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x80, 0xc9, 0x81, 0x00,
+ 0x00, 0x00, 0x00, 0x6d, 0x61, 0x6c, 0x77, 0x61, 0x72,
+ 0x65, 0x2e, 0x65, 0x78, 0x65, 0x2f, 0x2e, 0x2e, 0x75,
+ 0x70, 0x11, 0x00, 0x01, 0x7c, 0xbc, 0xe2, 0x5d, 0x64,
+ 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x2e, 0x74,
+ 0x78, 0x74, 0x50, 0x4b, 0x05, 0x06, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00,
+ 0x01, 0x00, 0x51, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+ std::string test_string(test_data, sizeof(test_data));
+ ZipReader reader;
+ ASSERT_TRUE(reader.OpenFromString(test_string));
+ const ZipReader::Entry* entry = reader.Next();
+ ASSERT_TRUE(entry);
+ EXPECT_EQ(base::FilePath::FromUTF8Unsafe("download.txt"), entry->path);
+ EXPECT_EQ(base::FilePath::FromUTF8Unsafe("malware.exe/UP"),
+ entry->physical_path);
+ EXPECT_FALSE(entry->is_directory);
+ EXPECT_TRUE(entry->is_unsafe);
+}
+
class FileWriterDelegateTest : public ::testing::Test {
protected:
void SetUp() override {
Original Bug Report
Safe Browsing bypass via Info-ZIP 0x7075 Unicode-Path differential in ZipReader
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential filename parser differential in Chromium’s ZIP reader allows attackers to bypass Safe Browsing executable detection. By utilizing the Info-ZIP Unicode-Path Extra Field (0x7075), an archive can present a benign filename (e.g., ‘.txt’) to Chromium’s ZIP analyzer while native Windows Explorer extracts it as an executable. This results in malicious executables bypassing download reputation analysis and reaching the victim’s filesystem.
Affected files:
third_party/zlib/google/zip_reader.ccchrome/utility/safe_browsing/zip_analyzer.ccchrome/common/safe_browsing/archive_analyzer_results.cc
Estimated timestamp from git blame: 2026-05-04
Root Cause Analysis
Chromium’s ZIP reader in third_party/zlib/google/zip_reader.cc relies on a custom-patched version of minizip (third_party/zlib/contrib/minizip/unzip.c) which implements parsing for the Info-ZIP Unicode Path Extra Field (0x7075, or ux).
In third_party/zlib/google/zip_reader.cc:ZipReader::OpenEntry(), the parser checks for the presence of this extra field and, if validated, unconditionally overrides entry_.path_in_original_encoding with the extra field’s UTF-8 path:
if (info.size_utf8_filename > 0) {
// Use the Info-ZIP Unicode Path Extra Field if present.
DCHECK(info.utf8_filename[info.size_utf8_filename] == 0);
entry_.path_in_original_encoding = info.utf8_filename;
}
For info.size_utf8_filename > 0 to be set, the following validation constraints in third_party/zlib/contrib/minizip/unzip.c must be met:
- The Extra Field ID must be
0x7075. - The extra field payload must contain a valid size (at least 5 bytes for version and CRC32).
- The version field must be
1. - The CRC32 checksum inside the extra field must match the CRC32 of the physical filename from the central directory.
Since an archive’s creator has full control over all of these values, an attacker can easily craft a ZIP where these criteria are met. This unconditional override creates a severe parser differential because default OS-level extraction tools (specifically Windows Explorer’s native zipfldr.dll) completely ignore the 0x7075 field and extract files using their standard Central Directory filename.
Potential Safe Browsing Bypass Vector
This differential allows an attacker to bypass executable reputation checks in Safe Browsing:
- Extension Bypass: An attacker packages a malicious Windows PE executable (
malware.exe) and appends a0x7075Unicode-Path Extra Field pointing to a non-executable filename, such asreceipt.txt. - Analysis Flow: During download protection analysis,
ZipAnalyzer::ResumeExtraction()iterates over the archive entries. Due to the override, the entry path is read asreceipt.txt. - Result Filtering:
ArchiveAnalyzer::UpdateResultsForEntry()invokesIsCheckedBinaryFile('receipt.txt')viaarchive_analyzer_results.cc. Because.txtis not a checked binary extension,IsCheckedBinaryFilereturnsfalse. No binary reputation analysis (e.g., hashing, PE header inspection) is performed for this file. - Download Allowed: The download is categorized as a safe archive and delivered to the user with no warnings.
- Extraction: When the user extracts the ZIP file using native Windows Explorer, the OS utility ignores the
0x7075extra field and extracts the file using its physical name, deliveringmalware.exeto the filesystem.
Potential Steps to Reproduce / Trigger
Note: These are suggested steps based on static analysis; our tooling agent does not currently have the capability to run code to verify this live.
- Create a ZIP archive containing a single compressed Windows PE executable payload.
- Set the Central Directory and Local File Header filenames to
malware.exe. - Append an Info-ZIP Unicode Path Extra Field (
0x7075) to the Central Directory entry containing:Header ID=0x7075Version=1NameCRC32=crc32('malware.exe')UnicodeName=receipt.txt
- Download the ZIP file via Chrome on Windows.
- Inspect
chrome://safe-browsingor the network log. TheClientDownloadRequestshould classify the download as a safeARCHIVEwith an emptyarchived_binarylist. - Extract the downloaded archive using native Windows Explorer to verify that the file is extracted as
malware.exe.
Fix Suggestion
To prevent parser differentials, the ZIP reader used by Safe Browsing should either:
- Validate both the standard Central Directory filename and the overridden Unicode-Path filename. If any restricted file extension (such as
.exe) is present in either field, the entry must be treated as a checked binary. - Alternatively, detect discrepancies between the standard Central Directory filename and the
0x7075extra field, and mark any mismatch as suspicious or treat the archive as untrusted.
Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.