Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper input validation in Safebrowsing
DescriptionImproper input validation in Safebrowsing
ComponentSafebrowsing
Bug ClassLogic Error
Tracker517074167
Fix commit4de51f777356 (chromium/src) +270/-5
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
TEST_F
chrome/browser/safe_browsing/download_protection/file_analyzer_unittest.cc
modified
if
chrome/utility/safe_browsing/zip_analyzer.cc
modified
if
third_party/zlib/google/zip_reader.cc
modified

Files Changed

  • chrome/browser/safe_browsing/download_protection/file_analyzer_unittest.cc
  • chrome/test/data/safe_browsing/download_protection/zipfile_unicode_path_extra_field.zip
  • chrome/utility/safe_browsing/zip_analyzer.cc
  • third_party/zlib/google/zip_reader.cc
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) {
Loading diff…

Regression Test / PoC

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

Original Bug Report

reported by [email protected]

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.cc
  • chrome/utility/safe_browsing/zip_analyzer.cc
  • chrome/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:

  1. The Extra Field ID must be 0x7075.
  2. The extra field payload must contain a valid size (at least 5 bytes for version and CRC32).
  3. The version field must be 1.
  4. 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:

  1. Extension Bypass: An attacker packages a malicious Windows PE executable (malware.exe) and appends a 0x7075 Unicode-Path Extra Field pointing to a non-executable filename, such as receipt.txt.
  2. Analysis Flow: During download protection analysis, ZipAnalyzer::ResumeExtraction() iterates over the archive entries. Due to the override, the entry path is read as receipt.txt.
  3. Result Filtering: ArchiveAnalyzer::UpdateResultsForEntry() invokes IsCheckedBinaryFile('receipt.txt') via archive_analyzer_results.cc. Because .txt is not a checked binary extension, IsCheckedBinaryFile returns false. No binary reputation analysis (e.g., hashing, PE header inspection) is performed for this file.
  4. Download Allowed: The download is categorized as a safe archive and delivered to the user with no warnings.
  5. Extraction: When the user extracts the ZIP file using native Windows Explorer, the OS utility ignores the 0x7075 extra field and extracts the file using its physical name, delivering malware.exe to 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.

  1. Create a ZIP archive containing a single compressed Windows PE executable payload.
  2. Set the Central Directory and Local File Header filenames to malware.exe.
  3. Append an Info-ZIP Unicode Path Extra Field (0x7075) to the Central Directory entry containing:
    • Header ID = 0x7075
    • Version = 1
    • NameCRC32 = crc32('malware.exe')
    • UnicodeName = receipt.txt
  4. Download the ZIP file via Chrome on Windows.
  5. Inspect chrome://safe-browsing or the network log. The ClientDownloadRequest should classify the download as a safe ARCHIVE with an empty archived_binary list.
  6. 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:

  1. 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.
  2. Alternatively, detect discrepancies between the standard Central Directory filename and the 0x7075 extra 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.

View on issue tracker