CVE-2026-79151
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fchrome/browser/safe_browsing/download_protection/file_analyzer_unittest.cc |
modified | |
ifchrome/utility/safe_browsing/zip_analyzer.cc |
modified | |
ifthird_party/zlib/google/zip_reader.cc |
modified |
Files Changed
chrome/browser/safe_browsing/download_protection/file_analyzer_unittest.ccchrome/test/data/safe_browsing/download_protection/zipfile_unicode_path_extra_field.zipchrome/utility/safe_browsing/zip_analyzer.ccthird_party/zlib/google/zip_reader.cc
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/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/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) {
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.