CVE-2026-11231
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TESTchrome/utility/safe_browsing/mac/dmg_analyzer_unittest.cc |
modified |
Files Changed
chrome/utility/safe_browsing/archive_analyzer.ccchrome/utility/safe_browsing/archive_analyzer.hchrome/utility/safe_browsing/mac/dmg_analyzer.ccchrome/utility/safe_browsing/mac/dmg_analyzer_unittest.cc
Patch
From d942e1c61a0ad15aa4b745eb087a390a00e2ebae Mon Sep 17 00:00:00 2001 From: Andrew Paseltiner <[email protected]> Date: Mon, 04 May 2026 06:11:21 -0700 Subject: [PATCH] Fix logic errors in DMGAnalyzer skipping nested archives This CL fixes two interconnected logic bugs in DMGAnalyzer that caused nested archives within DMG files to bypass Safe Browsing analysis: 1. In DMGAnalyzer::OnGetTempFile, the temp_file parameter was not being assigned to the temp_file_ member. 2. In DMGAnalyzer::ResumeExtraction, the return value of UpdateResultsForEntry was being inverted, causing the analyzer to incorrectly signal completion when it should have paused for nested analysis. Included is a regression test DMGAnalyzerTest.NestedArchive that verifies the fix by ensuring nested archives are correctly detected and processed. Fixed: 495840862 Change-Id: I7cfb0a6fe0e69bcb75d037a6258202cd80f65837 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7808264 Commit-Queue: Andrew Paseltiner <[email protected]> Reviewed-by: Xinghui Lu <[email protected]> Cr-Commit-Position: refs/heads/main@{#1624598} --- diff --git a/chrome/utility/safe_browsing/archive_analyzer.cc b/chrome/utility/safe_browsing/archive_analyzer.cc index 074c463..7aaf094 100644 --- a/chrome/utility/safe_browsing/archive_analyzer.cc +++ b/chrome/utility/safe_browsing/archive_analyzer.cc @@ -76,6 +76,11 @@ finished_analysis_callback_ = std::move(callback); } +void ArchiveAnalyzer::SetGetTempFileCallbackForTesting( + GetTempFileCallback callback) { + get_temp_file_callback_ = std::move(callback); +} + void ArchiveAnalyzer::SetAnalysisDelegate( std::unique_ptr<ArchiveAnalysisDelegate> analysis_delegate) { CHECK(analysis_delegate); diff --git a/chrome/utility/safe_browsing/archive_analyzer.h b/chrome/utility/safe_browsing/archive_analyzer.h index bc9a966..6f657e5 100644 --- a/chrome/utility/safe_browsing/archive_analyzer.h +++ b/chrome/utility/safe_browsing/archive_analyzer.h @@ -45,6 +45,7 @@ void SetResultsForTesting(ArchiveAnalyzerResults* results); void SetFinishedCallbackForTesting(FinishedAnalysisCallback callback); + void SetGetTempFileCallbackForTesting(GetTempFileCallback callback); void SetAnalysisDelegate( std::unique_ptr<ArchiveAnalysisDelegate> analysis_delegate); diff --git a/chrome/utility/safe_browsing/mac/dmg_analyzer.cc b/chrome/utility/safe_browsing/mac/dmg_analyzer.cc index cabf1eff..30c5b10 100644 --- a/chrome/utility/safe_browsing/mac/dmg_analyzer.cc +++ b/chrome/utility/safe_browsing/mac/dmg_analyzer.cc @@ -213,11 +213,13 @@ } // TODO(crbug.com/40871873): Support file length here. - return !UpdateResultsForEntry( - temp_file_.Duplicate(), GetRootPath().Append(path), - /*file_length=*/0, - /*is_encrypted=*/false, /*is_directory=*/false, - /*contents_valid=*/true); + if (!UpdateResultsForEntry( + temp_file_.Duplicate(), GetRootPath().Append(path), + /*file_length=*/0, + /*is_encrypted=*/false, /*is_directory=*/false, + /*contents_valid=*/true)) { + return false; + } } } } @@ -231,6 +233,8 @@ return; } + temp_file_ = std::move(temp_file); + if (!iterator_->Open()) { InitComplete(ArchiveAnalysisResult::kUnknown); return; diff --git a/chrome/utility/safe_browsing/mac/dmg_analyzer_unittest.cc b/chrome/utility/safe_browsing/mac/dmg_analyzer_unittest.cc index a13a158..1e0127c 100644 --- a/chrome/utility/safe_browsing/mac/dmg_analyzer_unittest.cc +++ b/chrome/utility/safe_browsing/mac/dmg_analyzer_unittest.cc @@ -200,6 +200,59 @@ EXPECT_EQ(0, results.detached_code_signatures.size()); } +// Regression test for crbug.com/495840862. This verifies that nested archives +// are correctly detected and processed by ensuring the analyzer saves the +// temporary file and correctly pauses extraction for nested analysis. +// Note: We expect results.success to be false because the nested archive +// provided in the test is intentionally invalid (it has no 7z header), which +// causes the nested analyzer (and thus the overall analysis) to report failure. +TEST(DMGAnalyzerTest, NestedArchive) { + base::test::TaskEnvironment task_environment; + DMGAnalyzer analyzer_; + base::FilePath temp_path; + base::File temp_file; + base::CreateTemporaryFile(&temp_path); + temp_file.Initialize( + temp_path, (base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_READ | + base::File::FLAG_WRITE | base::File::FLAG_WIN_TEMPORARY | + base::File::FLAG_DELETE_ON_CLOSE)); + + MockDMGIterator::FileList file_list{ + {"Nested.7z", {0x01, 0x02, 0x03, 0x04}}, + }; + + std::unique_ptr<MockDMGIterator> iterator = + std::make_unique<MockDMGIterator>(true, file_list); + safe_browsing::ArchiveAnalyzerResults results; + base::RunLoop run_loop; + + analyzer_.SetGetTempFileCallbackForTesting( + base::BindRepeating([](base::OnceCallback<void(base::File)> callback) { + base::FilePath path; + base::CreateTemporaryFile(&path); + base::File file(path, base::File::FLAG_CREATE_ALWAYS | + base::File::FLAG_READ | + base::File::FLAG_WRITE | + base::File::FLAG_DELETE_ON_CLOSE); + std::move(callback).Run(std::move(file)); + })); + + analyzer_.AnalyzeDMGFileForTesting(std::move(iterator), &results, + std::move(temp_file), + run_loop.QuitClosure()); + run_loop.Run(); + + // The analysis as a whole fails because the nested 7z is invalid. + EXPECT_FALSE(results.success); + // However, we verify the logic fix by checking that the archive was detected. + // If the logic fix were missing, has_archive would be false because the + // Nested.7z would have been skipped entirely. + EXPECT_TRUE(results.has_archive); + ASSERT_EQ(1u, results.archived_archive_filenames.size()); + EXPECT_EQ(FILE_PATH_LITERAL("Nested.7z"), + results.archived_archive_filenames[0].value()); +} + } // namespace } // namespace dmg } // namespace safe_browsing
Regression Test / PoC
diff --git a/chrome/utility/safe_browsing/mac/dmg_analyzer_unittest.cc b/chrome/utility/safe_browsing/mac/dmg_analyzer_unittest.cc
index a13a158..1e0127c 100644
--- a/chrome/utility/safe_browsing/mac/dmg_analyzer_unittest.cc
+++ b/chrome/utility/safe_browsing/mac/dmg_analyzer_unittest.cc
@@ -200,6 +200,59 @@
EXPECT_EQ(0, results.detached_code_signatures.size());
}
+// Regression test for crbug.com/495840862. This verifies that nested archives
+// are correctly detected and processed by ensuring the analyzer saves the
+// temporary file and correctly pauses extraction for nested analysis.
+// Note: We expect results.success to be false because the nested archive
+// provided in the test is intentionally invalid (it has no 7z header), which
+// causes the nested analyzer (and thus the overall analysis) to report failure.
+TEST(DMGAnalyzerTest, NestedArchive) {
+ base::test::TaskEnvironment task_environment;
+ DMGAnalyzer analyzer_;
+ base::FilePath temp_path;
+ base::File temp_file;
+ base::CreateTemporaryFile(&temp_path);
+ temp_file.Initialize(
+ temp_path, (base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_READ |
+ base::File::FLAG_WRITE | base::File::FLAG_WIN_TEMPORARY |
+ base::File::FLAG_DELETE_ON_CLOSE));
+
+ MockDMGIterator::FileList file_list{
+ {"Nested.7z", {0x01, 0x02, 0x03, 0x04}},
+ };
+
+ std::unique_ptr<MockDMGIterator> iterator =
+ std::make_unique<MockDMGIterator>(true, file_list);
+ safe_browsing::ArchiveAnalyzerResults results;
+ base::RunLoop run_loop;
+
+ analyzer_.SetGetTempFileCallbackForTesting(
+ base::BindRepeating([](base::OnceCallback<void(base::File)> callback) {
+ base::FilePath path;
+ base::CreateTemporaryFile(&path);
+ base::File file(path, base::File::FLAG_CREATE_ALWAYS |
+ base::File::FLAG_READ |
+ base::File::FLAG_WRITE |
+ base::File::FLAG_DELETE_ON_CLOSE);
+ std::move(callback).Run(std::move(file));
+ }));
+
+ analyzer_.AnalyzeDMGFileForTesting(std::move(iterator), &results,
+ std::move(temp_file),
+ run_loop.QuitClosure());
+ run_loop.Run();
+
+ // The analysis as a whole fails because the nested 7z is invalid.
+ EXPECT_FALSE(results.success);
+ // However, we verify the logic fix by checking that the archive was detected.
+ // If the logic fix were missing, has_archive would be false because the
+ // Nested.7z would have been skipped entirely.
+ EXPECT_TRUE(results.has_archive);
+ ASSERT_EQ(1u, results.archived_archive_filenames.size());
+ EXPECT_EQ(FILE_PATH_LITERAL("Nested.7z"),
+ results.archived_archive_filenames[0].value());
+}
+
} // namespace
} // namespace dmg
} // namespace safe_browsing
Original Bug Report
Potential Safe Browsing bypass for nested archives in DMGs due to logic errors in DMGAnalyzer
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: Two logic errors in DMGAnalyzer cause Safe Browsing to skip scanning nested archives within DMG files. A missing assignment in OnGetTempFile prevents nested extraction, and an inverted return value in ResumeExtraction prematurely signals analysis completion, allowing malicious payloads to evade detection.
Affected files:
chrome/utility/safe_browsing/mac/dmg_analyzer.cc
Estimated timestamp from git blame: 2023-07-26
Two interconnected logic bugs in chrome/utility/safe_browsing/mac/dmg_analyzer.cc result in nested archives within DMG files bypassing Safe Browsing analysis on macOS.
Bug 1: Missing assignment in DMGAnalyzer::OnGetTempFile
In chrome/utility/safe_browsing/mac/dmg_analyzer.cc, the method DMGAnalyzer::OnGetTempFile receives a base::File temp_file parameter but fails to assign it to the class member temp_file_. Consequently, temp_file_ remains an invalid, default-constructed handle.
When ResumeExtraction processes a nested archive entry (ZIP, RAR, 7z, or DMG), it attempts to extract it using CopyStreamToFile(*stream, temp_file_). Because the file handle is invalid, the underlying OS write operations fail (returning false in Release builds). This causes the loop to continue at line 208 or 212, silently skipping the nested archive entirely. Safe Browsing performs no further analysis on the skipped archive.
Bug 2: Inverted return logic in DMGAnalyzer::ResumeExtraction
If Bug 1 were fixed, a second issue exists on line 216 of dmg_analyzer.cc, which performs return !UpdateResultsForEntry(...). This inverts the contract used by sibling analyzers (such as ZIP, RAR, and 7z), which correctly use if (!UpdateResultsForEntry(...)) return false;.
When a nested archive is encountered, UpdateResultsForEntry initiates an asynchronous nested analyzer via Mojo and returns false to indicate that extraction is paused. Due to the logic inversion (!false), DMGAnalyzer returns true, incorrectly signaling to ArchiveAnalyzer::InitComplete that extraction is finished. This triggers finished_analysis_callback_ prematurely, sending a Mojo reply to the browser process before the nested scan completes.
The browser process then tears down the utility process, aborting the nested scan and resulting in a bypass. Furthermore, if the nested scan somehow finishes synchronously, it will trigger a CHECK(!is_null()) crash due to a use-after-move on the already-executed finished_analysis_callback_ (at archive_analyzer.cc:170).
Potential Attack Scenario
Note: Our setup does not have the ability to run code, so these are potential steps an attacker might follow:
- An attacker creates a DMG file containing a nested ZIP archive.
- Inside the nested ZIP archive, the attacker places a malicious payload (e.g., a malware executable).
- The attacker hosts the DMG file and tricks a macOS victim into downloading it via Chrome.
- Safe Browsing intercepts the download and sends it to the utility process for analysis.
- Due to Bug 1,
DMGAnalyzerfails to write the nested ZIP to a temporary file and silently skips it. - The analyzer reports the DMG as safe to the browser process, allowing the user to access the malicious payload.
Suggested Fix
- In
DMGAnalyzer::OnGetTempFile, assign the passedtemp_fileto the member variable:
void DMGAnalyzer::OnGetTempFile(base::File temp_file) {
if (!temp_file.IsValid()) {
InitComplete(ArchiveAnalysisResult::kFailedToOpenTempFile);
return;
}
temp_file_ = std::move(temp_file);
// ...
}
- In
DMGAnalyzer::ResumeExtraction, fix the inverted logic to match other analyzers:
if (!UpdateResultsForEntry(
temp_file_.Duplicate(), GetRootPath().Append(path),
/*file_length=*/0,
/*is_encrypted=*/false, /*is_directory=*/false,
/*contents_valid=*/true)) {
return false;
}
Evaluated with Chrome root at commit: 0eb4855bda702feaaa8b899336664f97e3df88b8
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. Please feel free to reach out to me if you have concerns or feedback.