Medium watch CVSS 4.3 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper input validation in Safebrowsing
DescriptionImproper input validation in Safebrowsing
ComponentSafebrowsing
Bug ClassInput Validation
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker517074167
Fix commit4de51f777356 (chromium/src) +270/-5
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-27 (per vendor advisory)
Fix landed2026-07-21 (public git)
Disclosed2026-08-25
Patch gap35 days — fix public in source before the advisory

Background

Info-ZIP Unicode Path Extra Field
an optional ZIP extra field (up, ID 0x7075) 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, whose path field is the name consumers act on and whose newly added physical_path field 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 UpdateResultsForEntry to build the Safe Browsing ClientDownloadRequest.

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.

Key insight
The single mistake was treating the Info-ZIP Unicode Path Extra Field name as the sole identity of an entry, letting an attacker split “what Safe Browsing checks” from “what gets extracted”; the fix preserves both names and classifies on whichever one is a checked binary or archive.

Attack Path

  1. Craft a dual-name entry Build a ZIP whose entry has Central Directory filename malware.exe (or payload.zip) but an Info-ZIP Unicode Path Extra Field advertising a benign name like receipt.txt, with a filename CRC that matches so the extra field is honored.
  2. Deliver the archive Get the victim to download the ZIP so Chrome’s download protection hands it to ZipAnalyzer in the utility process.
  3. Evade classification Pre-patch, ZipReader exposes only receipt.txt, so FileTypePolicies sees a harmless text file and the executable/archive entry is not reported as a ZIPPED_EXECUTABLE/ZIPPED_ARCHIVE, bypassing Safe Browsing inspection.
  4. Extract under the dangerous name A native extractor that ignores the Unicode Path Extra Field writes malware.exe to disk, so the user runs a binary that Safe Browsing never evaluated (inferred, based on the commit’s stated extractor divergence).

Impact Assessment

An attacker gains a Safe Browsing evasion: a malicious executable or nested archive inside a downloaded ZIP is misclassified as benign and skips download-protection scanning, in the browser’s download-protection path (analysis runs in the utility-process 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

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

Files Changed

  • third_party/zlib/google/zip_reader_unittest.cc
  • third_party/zlib/google/zip_reader.h
  • third_party/zlib/google/zip_reader.cc
  • chrome/test/data/safe_browsing/download_protection/zipfile_unicode_path_extra_field.zip
  • chrome/browser/safe_browsing/download_protection/file_analyzer_unittest.cc
  • chrome/utility/safe_browsing/zip_analyzer.cc

Audit Directions

  • Dual-name / metadata divergence
    Audit 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 mismatch
    Wherever 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 trust
    Review 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.
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 {
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