CVE-2026-87532
Overview
Background
- Safe Browsing archive analysis
- the Chrome feature that unpacks downloaded archives (e.g. RAR) and enumerates their contained files so each entry’s metadata can be checked for malicious executables.
- `RarReader` / UnRAR wrapper
- the code in
third_party/unrar/google/unrar_wrapper.ccthat drives UnRAR to walk a RAR archive one entry at a time and report each entry to the analyzer. - Dictionary (window) size limit
- UnRAR’s
command_->WinSizeLimit/command_->WinSizebound on an entry’s declared decompression window (FileHead.WinSize); an entry declaring a window larger than the limit (default 4 GiB) is refused for unpacking. - Entry enumeration return contract
- in the enumeration loop a
return trueafterarchive_->SeekToNext()means “this entry was reported, keep enumerating,” whereas returningfalsesignals end-of-archive and stops enumeration.
Root Cause Analysis
The commit message states that CmdExtract::ExtractCurrentFile() refuses to unpack an entry whose declared FileHead.WinSize exceeds the default limit and returns false, and that the enumeration routine (named RarReader::ExtractNextEntry() in the commit message) (inferred) propagated that false as end-of-archive.
As a result, any archive entries positioned after a large-dictionary entry were silently never reported to the analyzer, violating the invariant that enumeration should visit and report every entry in the archive.
The patch adds a new branch that recognizes the over-limit-dictionary case — archive_->FileHead.WinSize > command_->WinSizeLimit && archive_->FileHead.WinSize > command_->WinSize — and, instead of treating the refusal as end-of-archive, calls archive_->SeekToNext() and return true to skip only that one entry while continuing enumeration. This mirrors the existing handling for encrypted and split-volume entries (the surrounding SeekToNext(); return true; context), so the metadata of the skipped entry is still reported and subsequent entries are once again enumerated.
The fix works because it removes the conflation of “this entry cannot be unpacked” with “the archive has ended.”
Attack Path
- Craft a RAR with a poisoned middle entry
An attacker builds an archive whose entries are, in order, a benign file, a
dummy.binentry whose header declares an oversized decompression window (the test uses an 8 GiB dictionary, exceeding the 4 GiB default), and a malicious executable such asevil.exe. - Trigger analysis of the download The victim downloads the RAR, and Safe Browsing runs the sandboxed RAR analyzer, which drives the UnRAR wrapper to enumerate entries.
- Enumeration aborts early
On reaching the large-dictionary entry, UnRAR refuses to unpack it and returns
false; the pre-patch code propagated this as end-of-archive, so enumeration stopped andevil.exe(positioned after it) was never reported. - Malicious payload evades scanning (inferred)
Because the trailing executable is never enumerated, its metadata is not submitted for Safe Browsing verdicts, so the download’s malicious content can bypass detection.
(inferred)— the diff only demonstrates that the entry is now enumerated (via the added test asserting all three entries appear); the downstream detection-bypass consequence is not directly executed by the patch.
Impact Assessment
(inferred, exact process boundary not shown in diff). The precondition is only that the victim downloads and has Safe Browsing analyze such a crafted archive; no memory corruption is shown by the patch, which adds solely a skip-and-continue guard. The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 6.5. Any escalation beyond missed detection is (inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fchrome/services/file_util/public/cpp/sandboxed_rar_analyzer_unittest.cc |
modified |
Files Changed
chrome/services/file_util/public/cpp/sandboxed_rar_analyzer_unittest.ccthird_party/unrar/google/unrar_wrapper.ccchrome/test/data/safe_browsing/rar/large_dictionary.rar
Audit Directions
- Refusal-as-termination conflationAudit other archive/reader enumeration loops (RAR, ZIP, 7z, DMG analyzers) for places where a per-entry failure return is treated as end-of-stream, causing subsequent entries to be skipped from Safe Browsing analysis.
- Resource-limit early exitsLook for size/window/count limits (like
WinSizeLimit) whose enforcement aborts the whole walk rather than skipping the single offending entry, which attackers can weaponize as scan-evasion. - Divergence between unpack and enumerate pathsVerify that every condition under which the underlying library (
CmdExtract::ExtractCurrentFile()) refuses to unpack is mirrored by explicit skip-and-continue handling in the wrapper, matching the existing encrypted/split-volume cases.
Patch
From 1955b6410bd6d5ab09b9513ca55eebdb2c4a63cd Mon Sep 17 00:00:00 2001 From: thefrog <[email protected]> Date: Wed, 29 Jul 2026 14:28:29 -0700 Subject: [PATCH] [Safe Browsing] Continue RAR enumeration past large-dict entries UnRAR's CmdExtract::ExtractCurrentFile() refuses to unpack an entry whose declared dictionary size exceeds the default 4 GiB limit and returns false. RarReader::ExtractNextEntry() propagated that as end-of-archive, so any entries following the large-dictionary header were never reported by the analyzer. Handle this the same way as encrypted and split-volume entries: when extraction is refused for an over-limit dictionary, skip the entry but still report its metadata and continue enumerating. See the bug description for details on the contents of the archive large_dictionary.rar. Reviewed in https://crrev.com/i/9596797. TAG=agy CONV=4b630017-10db-4e6b-ba3e-fbfa9dd5e2b0 Fixed: 517369256 Change-Id: I3e8559e2b84ee7eae13da8bd720201738edc1d35 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8162728 Auto-Submit: thefrog <[email protected]> Commit-Queue: thefrog <[email protected]> SLSA-Policy-Verified: SLSA Policy Verification Service <[email protected]> Reviewed-by: Yaw Frempong <[email protected]> Cr-Commit-Position: refs/heads/main@{#1670599} --- diff --git a/third_party/unrar/google/unrar_wrapper.cc b/third_party/unrar/google/unrar_wrapper.cc index 762925a..7ea986f 100644 --- a/third_party/unrar/google/unrar_wrapper.cc +++ b/third_party/unrar/google/unrar_wrapper.cc @@ -139,6 +139,15 @@ archive_->SeekToNext(); return true; } + + if (archive_->FileHead.WinSize > command_->WinSizeLimit && + archive_->FileHead.WinSize > command_->WinSize) { + // The entry's declared dictionary size exceeds the configured limit, + // so unpacking was refused. Skip over this file but report the + // metadata we do have. + archive_->SeekToNext(); + return true; + } } } diff --git a/chrome/services/file_util/public/cpp/sandboxed_rar_analyzer_unittest.cc b/chrome/services/file_util/public/cpp/sandboxed_rar_analyzer_unittest.cc index 2993aa2..9004060 100644 --- a/chrome/services/file_util/public/cpp/sandboxed_rar_analyzer_unittest.cc +++ b/chrome/services/file_util/public/cpp/sandboxed_rar_analyzer_unittest.cc @@ -463,6 +463,34 @@ testing::Eq("evil.exe")))); } +TEST_F(SandboxedRarAnalyzerTest, AnalyzeRarWithLargeDictionaryEntry) { + // Verifies that an entry whose declared dictionary size exceeds UnRAR's + // default limit doesn't cause subsequent entries to be skipped. + // large_dictionary.rar contains: "readme.txt", "dummy.bin" (declares an + // 8 GiB dictionary), "evil.exe". + base::FilePath path; + ASSERT_NO_FATAL_FAILURE(path = GetFilePath("large_dictionary.rar")); + + safe_browsing::ArchiveAnalyzerResults results; + AnalyzeFile(path, &results); + + ASSERT_TRUE(results.success); + EXPECT_TRUE(results.has_executable); + // All three entries should be enumerated. + EXPECT_THAT( + results.archived_binary, + testing::UnorderedElementsAre( + testing::Property( + &safe_browsing::ClientDownloadRequest_ArchivedBinary::file_path, + testing::Eq("readme.txt")), + testing::Property( + &safe_browsing::ClientDownloadRequest_ArchivedBinary::file_path, + testing::Eq("dummy.bin")), + testing::Property( + &safe_browsing::ClientDownloadRequest_ArchivedBinary::file_path, + testing::Eq("evil.exe")))); +} + TEST_F(SandboxedRarAnalyzerTest, CanDeleteDuringExecution) { base::FilePath file_path; ASSERT_NO_FATAL_FAILURE(file_path = GetFilePath("small_archive.rar")); diff --git a/chrome/test/data/safe_browsing/rar/large_dictionary.rar b/chrome/test/data/safe_browsing/rar/large_dictionary.rar new file mode 100644 index 0000000..16ef55fc --- /dev/null +++ b/chrome/test/data/safe_browsing/rar/large_dictionary.rar Binary files differ
Regression Test / PoC
diff --git a/chrome/services/file_util/public/cpp/sandboxed_rar_analyzer_unittest.cc b/chrome/services/file_util/public/cpp/sandboxed_rar_analyzer_unittest.cc
index 2993aa2..9004060 100644
--- a/chrome/services/file_util/public/cpp/sandboxed_rar_analyzer_unittest.cc
+++ b/chrome/services/file_util/public/cpp/sandboxed_rar_analyzer_unittest.cc
@@ -463,6 +463,34 @@
testing::Eq("evil.exe"))));
}
+TEST_F(SandboxedRarAnalyzerTest, AnalyzeRarWithLargeDictionaryEntry) {
+ // Verifies that an entry whose declared dictionary size exceeds UnRAR's
+ // default limit doesn't cause subsequent entries to be skipped.
+ // large_dictionary.rar contains: "readme.txt", "dummy.bin" (declares an
+ // 8 GiB dictionary), "evil.exe".
+ base::FilePath path;
+ ASSERT_NO_FATAL_FAILURE(path = GetFilePath("large_dictionary.rar"));
+
+ safe_browsing::ArchiveAnalyzerResults results;
+ AnalyzeFile(path, &results);
+
+ ASSERT_TRUE(results.success);
+ EXPECT_TRUE(results.has_executable);
+ // All three entries should be enumerated.
+ EXPECT_THAT(
+ results.archived_binary,
+ testing::UnorderedElementsAre(
+ testing::Property(
+ &safe_browsing::ClientDownloadRequest_ArchivedBinary::file_path,
+ testing::Eq("readme.txt")),
+ testing::Property(
+ &safe_browsing::ClientDownloadRequest_ArchivedBinary::file_path,
+ testing::Eq("dummy.bin")),
+ testing::Property(
+ &safe_browsing::ClientDownloadRequest_ArchivedBinary::file_path,
+ testing::Eq("evil.exe"))));
+}
+
TEST_F(SandboxedRarAnalyzerTest, CanDeleteDuringExecution) {
base::FilePath file_path;
ASSERT_NO_FATAL_FAILURE(file_path = GetFilePath("small_archive.rar"));
diff --git a/chrome/test/data/safe_browsing/rar/large_dictionary.rar b/chrome/test/data/safe_browsing/rar/large_dictionary.rar
new file mode 100644
index 0000000..16ef55fc
--- /dev/null
+++ b/chrome/test/data/safe_browsing/rar/large_dictionary.rar
Binary files differ
Original Bug Report
Safe Browsing RAR analyzer bypass via large dictionary size header
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 logic flaw in Chromium’s Safe Browsing RAR analyzer allows a crafted RAR archive to completely bypass scanning. By declaring a large dictionary size that exceeds the 4 GB internal extraction limit, the analyzer terminates extraction early but erroneously reports a valid scan status. This allows any subsequent malicious payloads in the archive to completely escape detection.
Affected files:
third_party/unrar/src/extract.cppthird_party/unrar/google/unrar_wrapper.ccchrome/utility/safe_browsing/rar_analyzer.cc
Estimated timestamp from git blame: 2024-05-21
Potential Safe Browsing RAR Scan Bypass via Crafted File Headers
Within Chromium’s Safe Browsing RAR analyzer, a potential logic flaw in handling large dictionary sizes (FileHead.WinSize) can be exploited to terminate archive parsing prematurely while still reporting a successful analysis (ArchiveAnalysisResult::kValid). This enables subsequent files inside the archive (which may contain malicious payloads) to completely bypass Safe Browsing inspection.
Root Cause Analysis
When a RAR5/RAR7 archive file header declares a dictionary size (FileHead.WinSize) exceeding the default limit of 4 GB (WinSizeLimit), CmdExtract::ExtractCurrentFile() invokes CheckWinLimit(), which in turn calls uiDictLimit():
// third_party/unrar/src/extract.cpp
bool CmdExtract::CheckWinLimit(Archive &Arc,std::wstring &ArcFileName) {
if (Arc.FileHead.WinSize<=Cmd->WinSizeLimit || Arc.FileHead.WinSize<=Cmd->WinSize)
return true;
if (uiDictLimit(Cmd,ArcFileName,Arc.FileHead.WinSize,Max(Cmd->WinSizeLimit,Cmd->WinSize)))
Cmd->WinSizeLimit=Arc.FileHead.WinSize;
else {
ErrHandler.SetErrorCode(RARX_FATAL);
Arc.SeekToNext();
return false;
}
return true;
}
Since Chromium compiles unrar with the SILENT macro defined, the uiDictLimit implementation in third_party/unrar/src/uisilent.cpp unconditionally returns false:
// third_party/unrar/src/uisilent.cpp
bool uiDictLimit(CommandData *Cmd,const std::wstring &FileName,uint64 DictSize,uint64 MaxDictSize) {
return false; // Stop extracting.
}
Consequently, CheckWinLimit returns false, causing CmdExtract::ExtractCurrentFile to return false early (at third_party/unrar/src/extract.cpp:712).
An attacker can easily craft a dictionary size that is valid according to the RAR specification (which supports up to 64 GB via UNPACK_MAX_DICT) but exceeds Chromium’s internal 4 GB threshold. For instance, setting CompInfo to 0x4001 inside a RAR7 file header declares an 8 GB dictionary size. This bypasses the validation in arcread.cpp but triggers the early abort in CheckWinLimit.
Error Propagation Failure
When ExtractCurrentFile fails, the extraction loop in RarReader::ExtractNextEntry (third_party/unrar/google/unrar_wrapper.cc) terminates early and returns false:
bool RarReader::ExtractNextEntry() {
write_error_ = false;
bool success = true, repeat = true;
while (success || repeat) {
...
success = extractor_->ExtractCurrentFile(*archive_, header_size, repeat);
...
if (success) {
return true;
}
...
}
return false; // Returns false because success is false.
}
Crucially, in RarAnalyzer::ResumeExtraction (chrome/utility/safe_browsing/rar_analyzer.cc), the analyzer loop interprets ExtractNextEntry() == false as having successfully reached the end of the archive (EOF), without verifying if an extraction error occurred:
bool RarAnalyzer::ResumeExtraction() {
while (reader_.ExtractNextEntry()) {
...
}
if (reader_.HasWriteError()) {
results()->analysis_result = ArchiveAnalysisResult::kDiskError;
results()->success = false;
return false;
}
...
results()->success = true;
results()->analysis_result = ArchiveAnalysisResult::kValid;
return true;
}
Because write_error_ remains false (as no write/disk error occurred), the entire analyzer marks the scan as successful (ArchiveAnalysisResult::kValid, success = true). Any files stored after the large dictionary entry in the RAR archive are completely skipped and ignored during inspection, while the scan is reported as fully valid.
At the same time, standard desktop extraction utilities (like 64-bit WinRAR or 7-Zip) running on a user’s machine do not have this rigid, silent 4 GB limit and will successfully unpack all files in the archive (including any subsequent malicious binaries).
Potential Step-by-Step Reproduction Flow
(Please note: the following steps are based on static analysis of the source code. Our tooling agent does not have the ability to compile or run the code dynamically to verify with an active exploit POC.)
- Archive Construction: Construct a sequential RAR5/RAR7 archive containing:
- Entry 0:
readme.txt(a benign text file). - Entry 1:
dummy.bin(a crafted file header withCompInfo = 0x4001, declaring an 8 GB dictionary size). - Entry 2:
evil.exe(the malicious payload).
- Entry 0:
- Download: Download this RAR archive in Google Chrome.
- Safe Browsing Scan: Chrome sends the archive to the sandboxed utility process for analysis via
RarAnalyzer. - Early Exit: Extraction fails on
dummy.binbecause of the 8 GB dictionary size.RarAnalyzerinterprets the resultingfalsefromExtractNextEntryas successful completion of the archive, completely skipping overevil.exe. - Clean Result: Safe Browsing reports a valid scan with
readme.txtas the only content, permitting the download without any warning.
Suggested Fix
To resolve this issue, the Safe Browsing RAR analyzer should explicitly distinguish between a normal EOF and an extraction/iteration failure.
We suggest modifying RarReader to expose whether any fatal errors were recorded by ErrHandler (such as ErrHandler.GetErrorCode() != RARX_SUCCESS) after iteration finishes. If a fatal extraction error is detected, ResumeExtraction() should propagate the failure (e.g., reporting ArchiveAnalysisResult::kFailedDuringIteration or ArchiveAnalysisResult::kUnknown) and set success = false, rather than treating the early abort as a clean, successful scan.
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.