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.
Tracker513222422
Fix commit9fd876973543 (chromium/src) +34/-1
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-14 (per vendor advisory)
Fix landed2026-07-23 (public git)
Disclosed2026-08-25
Patch gap33 days — fix public in source before the advisory

Background

Archive deep inspection
Safe Browsing’s utility-process step that walks the entries of a downloaded archive (.7z, .zip, etc.) to detect executables and nested archives before allowing the download.
`ArchiveAnalyzer`
the base analyzer that iterates archive entries and, per entry, may spawn a nested analyzer to recurse into an entry that is itself an archive.
Nested analyzer
a child ArchiveAnalyzer created by ArchiveAnalyzer::CreateForArchiveType() and pointed at a temp file, used to inspect an entry whose file type looks like an archive.
Directory entry
a 7z/archive member that represents a folder rather than a file; its name can still carry an archive-looking extension (e.g. folder.zip).

Root Cause Analysis

In ArchiveAnalyzer::UpdateResultsForEntry(), the decision to recurse into an entry was gated only by if (!is_encrypted), then keyed on the entry’s file extension via GetFileType(path). Because the is_directory flag was ignored, a directory entry whose name has an archive extension (such as folder.zip) was treated as a real nested archive, and a nested analyzer was created and run against the temp file — a file that holds the previous entry’s bytes or is empty, since a directory carries no content of its own. The commit message notes that SevenZipAnalyzer::OnDirectory() also failed to record that this nested work was pending, so the outer analysis returned early and never inspected any entries appearing after the misclassified directory. The invariant violated is that only actual file entries with archive content should trigger recursion; a content-less directory should not.

The fix tightens the guard to if (!is_encrypted && !is_directory), so directory entries are never sent to a nested analyzer and the sequential inspection of later entries proceeds.

Key insight
The single mistake was selecting a nested analyzer purely from the entry’s file extension while ignoring whether the entry was a directory, letting a directory named folder.zip short-circuit the whole scan; the fix simply excludes directory entries from nested analysis.

Attack Path

  1. Craft the archive An attacker builds a 7z archive containing a directory entry whose name carries an archive extension (e.g. folder.zip) placed before a malicious executable entry (e.g. file.exe).
  2. Trigger deep inspection A victim downloads the archive, and Safe Browsing invokes ArchiveAnalyzer::UpdateResultsForEntry() on each entry during deep inspection.
  3. Misclassify the directory For the folder.zip directory entry, the old if (!is_encrypted) check passes and CreateForArchiveType(GetFileType(path)) spawns a nested analyzer against the temp file (empty or holding a prior entry’s bytes).
  4. Abort the scan early Because SevenZipAnalyzer::OnDirectory() did not mark nested work as pending, the overall analysis returns before inspecting the later file.exe entry, so the executable is never scanned.
  5. Evade detection The malicious executable reaches the user without Safe Browsing having deep-inspected it, bypassing download protection (inferred that this enables delivery of otherwise-flagged malware).

Impact Assessment

An attacker gains a Safe Browsing deep-inspection bypass: by inserting a directory entry with an archive-looking name, later entries — including executables — go uninspected, weakening download protection in the file-analysis utility process. The precondition is that the victim downloads the crafted archive and Safe Browsing deep inspection runs on it. The advisory classifies this as improper input validation in Safebrowsing; no in-the-wild exploitation is stated, and any downstream malware delivery is (inferred).

Changed Functions

FunctionChangeNotes
if
chrome/utility/safe_browsing/archive_analyzer.cc
modified
TEST_F
chrome/services/file_util/public/cpp/sandboxed_seven_zip_analyzer_unittest.cc
modified

Files Changed

  • third_party/lzma_sdk/google/test_data/archive_named_folder.7z
  • third_party/lzma_sdk/google/seven_zip_reader_unittest.cc
  • chrome/services/file_util/public/cpp/sandboxed_seven_zip_analyzer_unittest.cc
  • chrome/utility/safe_browsing/archive_analyzer.cc

Audit Directions

  • Extension-only type dispatch
    Look for archive/file handlers that pick a parser or recursion target from a name/extension alone without also checking entry kind (directory vs file) or actual content.
  • Early-exit on unaccounted async work
    Audit analyzers that spawn nested/asynchronous work without recording it as pending, where a missed pending flag lets the parent return early and skip remaining items.
  • Directory entries in archive readers
    Review other archive analyzers (SevenZipAnalyzer, ZIP, RAR handlers) for whether directory entries are consistently excluded from content extraction and nested inspection.
From 9fd8769735435ca0e9ca2e323e0909bf68bee7d4 Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <[email protected]>
Date: Thu, 23 Jul 2026 12:40:52 -0700
Subject: [PATCH] Don't recurse into directories during archive deep inspection

ArchiveAnalyzer::UpdateResultsForEntry() chose a nested analyzer based
only on the entry's file extension, so a 7z directory entry whose name
has an archive extension (e.g. "folder.zip") spawned a nested analyzer
against the temp file. This temp file holds the previous entry's bytes
or is empty. SevenZipAnalyzer::OnDirectory() also did not record that
nested work was pending, so the overall analysis returned early without
inspecting any later entries in the archive.

Fixed: 513222422
Change-Id: Icce57e5dc57211aaf98c970d4d40367e7795e547
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8132854
SLSA-Policy-Verified: SLSA Policy Verification Service <[email protected]>
Reviewed-by: thefrog <[email protected]>
Commit-Queue: Andrew Paseltiner <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1667330}
---
diff --git a/chrome/utility/safe_browsing/archive_analyzer.cc b/chrome/utility/safe_browsing/archive_analyzer.cc
index 7aaf094..49e7ae8 100644
--- a/chrome/utility/safe_browsing/archive_analyzer.cc
+++ b/chrome/utility/safe_browsing/archive_analyzer.cc
@@ -104,7 +104,7 @@
                                             bool is_encrypted,
                                             bool is_directory,
                                             bool contents_valid) {
-  if (!is_encrypted) {
+  if (!is_encrypted && !is_directory) {
     nested_analyzer_ = ArchiveAnalyzer::CreateForArchiveType(GetFileType(path));
     if (nested_analyzer_) {
       // Archive analyzers expect to start at the beginning of the
diff --git a/third_party/lzma_sdk/google/test_data/archive_named_folder.7z b/third_party/lzma_sdk/google/test_data/archive_named_folder.7z
new file mode 100644
index 0000000..de4becb9
--- /dev/null
+++ b/third_party/lzma_sdk/google/test_data/archive_named_folder.7z
Binary files differ
diff --git a/chrome/services/file_util/public/cpp/sandboxed_seven_zip_analyzer_unittest.cc b/chrome/services/file_util/public/cpp/sandboxed_seven_zip_analyzer_unittest.cc
index 1c0037b..ad9ad97 100644
--- a/chrome/services/file_util/public/cpp/sandboxed_seven_zip_analyzer_unittest.cc
+++ b/chrome/services/file_util/public/cpp/sandboxed_seven_zip_analyzer_unittest.cc
@@ -163,6 +163,35 @@
   EXPECT_FALSE(results.archived_binary[2].is_archive());
 }
 
+TEST_F(SandboxedSevenZipAnalyzerTest, BinaryAfterArchiveNamedFolder) {
+  safe_browsing::ArchiveAnalyzerResults results;
+  RunAnalyzer(
+      dir_test_data_.Append(FILE_PATH_LITERAL("archive_named_folder.7z")),
+      &results);
+  ASSERT_TRUE(results.success);
+  EXPECT_TRUE(results.has_executable);
+  EXPECT_TRUE(results.has_archive);
+  EXPECT_EQ(1, results.file_count);
+  EXPECT_EQ(1, results.directory_count);
+  ASSERT_EQ(2, results.archived_binary.size());
+
+  EXPECT_EQ("folder.zip", results.archived_binary[0].file_path());
+  EXPECT_EQ(ClientDownloadRequest::ARCHIVE,
+            results.archived_binary[0].download_type());
+  EXPECT_FALSE(results.archived_binary[0].is_executable());
+  EXPECT_TRUE(results.archived_binary[0].is_archive());
+
+  EXPECT_EQ("file.exe", results.archived_binary[1].file_path());
+  EXPECT_EQ(ClientDownloadRequest::WIN_EXECUTABLE,
+            results.archived_binary[1].download_type());
+  EXPECT_EQ("B32E028F9B83C5FFB806CA7DFE7A3ECE5F1AED5A0368B0A140B35A67F5B000B3",
+            base::HexEncode(results.archived_binary[1].digests().sha256()));
+  EXPECT_EQ(19, results.archived_binary[1].length());
+  EXPECT_FALSE(results.archived_binary[1].is_encrypted());
+  EXPECT_TRUE(results.archived_binary[1].is_executable());
+  EXPECT_FALSE(results.archived_binary[1].is_archive());
+}
+
 TEST_F(SandboxedSevenZipAnalyzerTest, NestedArchive) {
   safe_browsing::ArchiveAnalyzerResults results;
   RunAnalyzer(dir_test_data_.Append(FILE_PATH_LITERAL("inner_archive.7z")),
diff --git a/third_party/lzma_sdk/google/seven_zip_reader_unittest.cc b/third_party/lzma_sdk/google/seven_zip_reader_unittest.cc
index 188180a..b291d9a 100644
--- a/third_party/lzma_sdk/google/seven_zip_reader_unittest.cc
+++ b/third_party/lzma_sdk/google/seven_zip_reader_unittest.cc
@@ -31,6 +31,10 @@
 //   values with 0.
 //
 //   echo "This is not an exe" > file.exe
+//   mkdir folder.zip
+//   7z a archive_named_folder.7z file.exe folder.zip
+//
+//   echo "This is not an exe" > file.exe
 //   7z a -p encrypted.7z file.exe  # Provided 1234 as the password
 //
 //   echo "This is not an exe" > file.exe
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/services/file_util/public/cpp/sandboxed_seven_zip_analyzer_unittest.cc b/chrome/services/file_util/public/cpp/sandboxed_seven_zip_analyzer_unittest.cc
index 1c0037b..ad9ad97 100644
--- a/chrome/services/file_util/public/cpp/sandboxed_seven_zip_analyzer_unittest.cc
+++ b/chrome/services/file_util/public/cpp/sandboxed_seven_zip_analyzer_unittest.cc
@@ -163,6 +163,35 @@
   EXPECT_FALSE(results.archived_binary[2].is_archive());
 }
 
+TEST_F(SandboxedSevenZipAnalyzerTest, BinaryAfterArchiveNamedFolder) {
+  safe_browsing::ArchiveAnalyzerResults results;
+  RunAnalyzer(
+      dir_test_data_.Append(FILE_PATH_LITERAL("archive_named_folder.7z")),
+      &results);
+  ASSERT_TRUE(results.success);
+  EXPECT_TRUE(results.has_executable);
+  EXPECT_TRUE(results.has_archive);
+  EXPECT_EQ(1, results.file_count);
+  EXPECT_EQ(1, results.directory_count);
+  ASSERT_EQ(2, results.archived_binary.size());
+
+  EXPECT_EQ("folder.zip", results.archived_binary[0].file_path());
+  EXPECT_EQ(ClientDownloadRequest::ARCHIVE,
+            results.archived_binary[0].download_type());
+  EXPECT_FALSE(results.archived_binary[0].is_executable());
+  EXPECT_TRUE(results.archived_binary[0].is_archive());
+
+  EXPECT_EQ("file.exe", results.archived_binary[1].file_path());
+  EXPECT_EQ(ClientDownloadRequest::WIN_EXECUTABLE,
+            results.archived_binary[1].download_type());
+  EXPECT_EQ("B32E028F9B83C5FFB806CA7DFE7A3ECE5F1AED5A0368B0A140B35A67F5B000B3",
+            base::HexEncode(results.archived_binary[1].digests().sha256()));
+  EXPECT_EQ(19, results.archived_binary[1].length());
+  EXPECT_FALSE(results.archived_binary[1].is_encrypted());
+  EXPECT_TRUE(results.archived_binary[1].is_executable());
+  EXPECT_FALSE(results.archived_binary[1].is_archive());
+}
+
 TEST_F(SandboxedSevenZipAnalyzerTest, NestedArchive) {
   safe_browsing::ArchiveAnalyzerResults results;
   RunAnalyzer(dir_test_data_.Append(FILE_PATH_LITERAL("inner_archive.7z")),
diff --git a/third_party/lzma_sdk/google/seven_zip_reader_unittest.cc b/third_party/lzma_sdk/google/seven_zip_reader_unittest.cc
index 188180a..b291d9a 100644
--- a/third_party/lzma_sdk/google/seven_zip_reader_unittest.cc
+++ b/third_party/lzma_sdk/google/seven_zip_reader_unittest.cc
@@ -31,6 +31,10 @@
 //   values with 0.
 //
 //   echo "This is not an exe" > file.exe
+//   mkdir folder.zip
+//   7z a archive_named_folder.7z file.exe folder.zip
+//
+//   echo "This is not an exe" > file.exe
 //   7z a -p encrypted.7z file.exe  # Provided 1234 as the password
 //
 //   echo "This is not an exe" > file.exe
Loading diff…

Original Bug Report

reported by [email protected]

Safe Browsing deep-scanning bypass in 7z analyzer via directory entries

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A logic error in the 7z archive analyzer allows specifically crafted archives to bypass Safe Browsing deep-scanning. By including a directory entry with an archive-type extension (e.g., ‘.zip’), an attacker can cause the analysis process to terminate prematurely. This results in subsequent malicious files within the same 7z archive being ignored by the security scanner.

Affected files:

  • chrome/utility/safe_browsing/seven_zip_analyzer.cc
  • chrome/utility/safe_browsing/archive_analyzer.cc
  • third_party/lzma_sdk/google/seven_zip_reader.cc

Estimated timestamp from git blame: 2023-04-26

Summary

A potential vulnerability has been identified in the Safe Browsing 7z analyzer where the extraction process can be tricked into finishing early. This allows an attacker to hide malicious binaries within a 7z archive, bypassing hash-blocklist and heuristic checks during deep scanning.

Potential Root Cause

The issue appears to reside in SevenZipAnalyzer::OnDirectory within chrome/utility/safe_browsing/seven_zip_analyzer.cc.

When the analyzer encounters an entry, it calls UpdateResultsForEntry in chrome/utility/safe_browsing/archive_analyzer.cc. This method checks the file extension to decide if it should perform a nested analysis (e.g., scanning a .zip inside the .7z). For a directory named with an archive extension (like folder.zip), it initiates a nested analysis and returns false, signaling that extraction should pause for asynchronous work.

However, in SevenZipAnalyzer::OnDirectory, the code fails to set the awaiting_nested_ flag to true when UpdateResultsForEntry returns false:

// chrome/utility/safe_browsing/seven_zip_analyzer.cc
bool SevenZipAnalyzer::OnDirectory(const seven_zip::EntryInfo& entry) {
  return UpdateResultsForEntry(
      temp_file_.Duplicate(), GetRootPath().Append(entry.file_path),
      entry.file_size, entry.is_encrypted, /*is_directory=*/true,
      /*contents_valid=*/!entry.is_encrypted);
} // BUG: awaiting_nested_ is never set to true here.

When this returns false to the underlying SevenZipReader, the extraction loop breaks. Control returns to SevenZipAnalyzer::ResumeExtraction(), which checks awaiting_nested_. Because the flag is false, it returns true, indicating completion to the parent ArchiveAnalyzer. This causes the analyzer to report final results to the browser process prematurely, before subsequent entries in the 7z archive have been inspected.

Furthermore, if the nested analysis eventually finishes, it may trigger a CHECK failure in the utility process because the completion callback (finished_analysis_callback_) has already been consumed and nullified during the premature completion.

Potential Steps to Reproduce

  1. Create a 7z archive where the first entry is a directory named bypass.zip.
  2. Add a known-malicious executable (e.g., a test EICAR file or malware.exe) as the second entry in the archive.
  3. Download the archive using a Chrome instance with Safe Browsing ‘Enhanced Protection’ or Deep Scanning enabled.
  4. Observe the Safe Browsing logs (e.g., via chrome://safe-browsing) to see if the analysis report includes the malicious executable. If the bypass is successful, the report will likely show a valid archive with no identified binaries.

Suggested Fix

In chrome/utility/safe_browsing/seven_zip_analyzer.cc, update OnDirectory to correctly set the awaiting_nested_ flag when UpdateResultsForEntry returns false, consistent with how it is handled in EntryDone:

bool SevenZipAnalyzer::OnDirectory(const seven_zip::EntryInfo& entry) {
  if (!UpdateResultsForEntry(
          temp_file_.Duplicate(), GetRootPath().Append(entry.file_path),
          entry.file_size, entry.is_encrypted, /*is_directory=*/true,
          /*contents_valid=*/!entry.is_encrypted)) {
    awaiting_nested_ = true;
    return false;
  }
  return true;
}

Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e


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