CVE-2026-87645
Overview
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.ccchrome/test/data/safe_browsing/rar/encrypted_high_kdf.rarthird_party/unrar/google/unrar_wrapper.cc
Patch
From 96dfd842b1413173623925072864a326d629577d Mon Sep 17 00:00:00 2001
From: Sarah Krakowiak Criel <skrakowi@chromium.org>
Date: Thu, 30 Jul 2026 12:14:33 -0700
Subject: [PATCH] unrar: skip key derivation when no password is supplied
When opening header-encrypted RAR archives without a caller-supplied
password, UnRAR attempts decryption using a dummy placeholder password.
This causes UnRAR to execute PBKDF2-HMAC-SHA256 for every encrypted
header and entry, even though decryption will never succeed. On archives
specifying the maximum KDF iteration exponent (2^24) and distinct salts,
this wastes significant CPU time on the sequenced task runner.
Set UnRAR's SkipEncrypted option when no password is provided so that
encrypted archive headers are reported as undecryptable immediately
without running KDF. Also short-circuit ExtractNextEntry for encrypted
file headers when password is empty to take the metadata reporting path
directly without attempting key derivation.
Add a unit test (AnalyzeEncryptedRarMaxIterationCount) and test archive
to verify entries are enumerated quickly without executing key
derivation.
The test archive
(chrome/test/data/safe_browsing/rar/encrypted_high_kdf.rar) was
generated by constructing a RAR5 archive with 8 dummy encrypted file
entries (e0.dat .. e7.dat) and 1 trailing unencrypted executable payload
(payload.exe). Each encrypted entry's FHEXTRA_CRYPT record was set to
the maximum KDF iteration exponent (Lg2Count = 24, corresponding to 2^24
= 16,777,216 PBKDF2 iterations) with distinct 16-byte salts.
TAG=agy
Bug: 502611474
Change-Id: Idc77ca1d39970cf17c2f8f5565e66c12bf496b9e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8160612
Reviewed-by: Yaw Frempong <yawfrempong@google.com>
Commit-Queue: Sarah Krakowiak <skrakowi@chromium.org>
SLSA-Policy-Verified: SLSA Policy Verification Service <devtools-gerritcodereview-exitgate@google.com>
Auto-Submit: Sarah Krakowiak <skrakowi@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1671306}
---
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 9004060..15c3c5e 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
@@ -281,6 +281,34 @@
EncryptionInfo::kKnownIncorrect);
}
+TEST_F(SandboxedRarAnalyzerTest, AnalyzeEncryptedRarMaxIterationCount) {
+ // Can quickly enumerate entries in an archive whose encrypted file headers
+ // specify the maximum permitted KDF iteration count, even though no
+ // password is supplied. Key derivation is unnecessary in this case because
+ // there is no candidate password to verify.
+ // encrypted_high_kdf.rar contains: e0.dat .. e7.dat (encrypted), payload.exe
+ base::FilePath path;
+ ASSERT_NO_FATAL_FAILURE(path = GetFilePath("encrypted_high_kdf.rar"));
+
+ safe_browsing::ArchiveAnalyzerResults results;
+ AnalyzeFile(path, &results);
+
+ ASSERT_TRUE(results.success);
+ EXPECT_TRUE(results.has_executable);
+ ASSERT_EQ(results.archived_binary.size(), 9);
+ EXPECT_EQ(results.archived_binary[0].file_path(), "e0.dat");
+ EXPECT_TRUE(results.archived_binary[0].is_encrypted());
+ EXPECT_EQ(results.archived_binary[7].file_path(), "e7.dat");
+ EXPECT_TRUE(results.archived_binary[7].is_encrypted());
+ EXPECT_EQ(results.archived_binary[8].file_path(), "payload.exe");
+ EXPECT_TRUE(results.archived_binary[8].is_executable());
+ EXPECT_FALSE(results.archived_binary[8].is_encrypted());
+
+ EXPECT_TRUE(results.encryption_info.is_encrypted);
+ EXPECT_EQ(results.encryption_info.password_status,
+ EncryptionInfo::kKnownIncorrect);
+}
+
TEST_F(SandboxedRarAnalyzerTest, AnalyzeRarWithPasswordMultipleFiles) {
// Can list files inside an archive that has password protected data.
// passwd1234_two_files.rar contains 2 files: signed.exe and text.txt
diff --git a/chrome/test/data/safe_browsing/rar/encrypted_high_kdf.rar b/chrome/test/data/safe_browsing/rar/encrypted_high_kdf.rar
new file mode 100644
index 0000000..6e215af
--- /dev/null
+++ b/chrome/test/data/safe_browsing/rar/encrypted_high_kdf.rar
Binary files differ
diff --git a/third_party/unrar/google/unrar_wrapper.cc b/third_party/unrar/google/unrar_wrapper.cc
index 7ea986f..6fd9e9b9 100644
--- a/third_party/unrar/google/unrar_wrapper.cc
+++ b/third_party/unrar/google/unrar_wrapper.cc
@@ -65,6 +65,14 @@
command_->ParseArg(password_flag.data());
command_->ParseArg(const_cast<wchar_t*>(L"t"));
command_->ParseDone();
+ if (password_.empty()) {
+ // Without a real password there is nothing useful key derivation can
+ // produce, so tell UnRAR to skip encrypted headers and entries instead of
+ // attempting decryption with the dummy password above. The work avoided
+ // scales with the archive's KDF iteration parameter and the number of
+ // distinct salts.
+ command_->SkipEncrypted = true;
+ }
// Disables an optimization that can allow specially crafted archives to
// bypass analysis. See crbug.com/506473226.
command_->Recurse = RECURSE_ALWAYS;
@@ -100,8 +108,15 @@
temp_file_.SetLength(0);
size_t header_size = archive_->ReadHeader();
repeat = false;
- success = extractor_->ExtractCurrentFile(
- *archive_, header_size, repeat); // |repeat| is passed by reference
+ // When no password was supplied, attempting to decrypt the entry will
+ // never succeed; skip extraction and report the available metadata.
+ bool skip_encrypted_entry = password_.empty() &&
+ archive_->GetHeaderType() == HEAD_FILE &&
+ archive_->FileHead.Encrypted;
+ success =
+ !skip_encrypted_entry &&
+ extractor_->ExtractCurrentFile(
+ *archive_, header_size, repeat); // |repeat| is passed by reference
if (archive_->GetHeaderType() == HEAD_FILE) {
#if defined(OS_WIN)
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 9004060..15c3c5e 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
@@ -281,6 +281,34 @@
EncryptionInfo::kKnownIncorrect);
}
+TEST_F(SandboxedRarAnalyzerTest, AnalyzeEncryptedRarMaxIterationCount) {
+ // Can quickly enumerate entries in an archive whose encrypted file headers
+ // specify the maximum permitted KDF iteration count, even though no
+ // password is supplied. Key derivation is unnecessary in this case because
+ // there is no candidate password to verify.
+ // encrypted_high_kdf.rar contains: e0.dat .. e7.dat (encrypted), payload.exe
+ base::FilePath path;
+ ASSERT_NO_FATAL_FAILURE(path = GetFilePath("encrypted_high_kdf.rar"));
+
+ safe_browsing::ArchiveAnalyzerResults results;
+ AnalyzeFile(path, &results);
+
+ ASSERT_TRUE(results.success);
+ EXPECT_TRUE(results.has_executable);
+ ASSERT_EQ(results.archived_binary.size(), 9);
+ EXPECT_EQ(results.archived_binary[0].file_path(), "e0.dat");
+ EXPECT_TRUE(results.archived_binary[0].is_encrypted());
+ EXPECT_EQ(results.archived_binary[7].file_path(), "e7.dat");
+ EXPECT_TRUE(results.archived_binary[7].is_encrypted());
+ EXPECT_EQ(results.archived_binary[8].file_path(), "payload.exe");
+ EXPECT_TRUE(results.archived_binary[8].is_executable());
+ EXPECT_FALSE(results.archived_binary[8].is_encrypted());
+
+ EXPECT_TRUE(results.encryption_info.is_encrypted);
+ EXPECT_EQ(results.encryption_info.password_status,
+ EncryptionInfo::kKnownIncorrect);
+}
+
TEST_F(SandboxedRarAnalyzerTest, AnalyzeRarWithPasswordMultipleFiles) {
// Can list files inside an archive that has password protected data.
// passwd1234_two_files.rar contains 2 files: signed.exe and text.txt
diff --git a/chrome/test/data/safe_browsing/rar/encrypted_high_kdf.rar b/chrome/test/data/safe_browsing/rar/encrypted_high_kdf.rar
new file mode 100644
index 0000000..6e215af
--- /dev/null
+++ b/chrome/test/data/safe_browsing/rar/encrypted_high_kdf.rar
Binary files differ
Original Bug Report
Safe Browsing bypass via RAR PBKDF2 iteration-count exhaustion
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.
Overview: A logic flaw in Chrome’s Safe Browsing RAR analysis allows attackers to bypass security warnings. By using maximum PBKDF2 iteration counts in encrypted entries, an attacker can cause a synchronous hang in the utility process. This leads to a browser-side timeout and a fail-open result, allowing malicious files to be downloaded without a warning.
Affected files:
chrome/utility/safe_browsing/rar_analyzer.ccchrome/services/file_util/safe_archive_analyzer.ccthird_party/unrar/src/crypt.hppthird_party/unrar/google/unrar_wrapper.ccchrome/browser/download/chrome_download_manager_delegate.ccchrome/browser/safe_browsing/download_protection/check_client_download_request_base.ccthird_party/unrar/src/arcread.cppthird_party/unrar/src/crypt5.cppthird_party/unrar/src/extract.cppcomponents/safe_browsing/content/resources/download_file_types.asciipb
Estimated timestamp from git blame: 2025-12-17
Summary
A potential vulnerability exists in the RAR archive analysis within Safe Browsing. A crafted RAR5 archive with high KDF iteration counts can block the utility process’s main thread. This prevents the analysis from completing, triggering a browser-side timeout that defaults to a safe classification (fail-open), thereby bypassing malware checks.
Technical Details
The bypass occurs due to the intersection of several behaviors across the unrar library and Chrome’s archive analysis architecture:
- High KDF Iteration Limit: In
third_party/unrar/src/crypt.hpp,CRYPT5_KDF_LG2_COUNT_MAXis set to 24 in production builds. This allows an archive to specify $2^{24}$ (approximately 16.7 million) PBKDF2 iterations per encrypted file. - Unconditional PBKDF2 Execution: When Safe Browsing analyzes an archive,
third_party/unrar/google/unrar_wrapper.ccprovides a default dummy password (“x”) if no password is provided by the user. The wrapper attempts to extract every entry by callingextractor_->ExtractCurrentFile. This unconditionally triggers the synchronous PBKDF2 loop inthird_party/unrar/src/crypt5.cppfor every encrypted entry with a unique salt. Without hardware acceleration, this takes 10-15 seconds per entry. - Blocked Synchronous Watchdog: The extraction loop runs synchronously on the utility process’s main sequence. The 10-second watchdog timer (
kArchiveAnalysisTimeoutinSafeArchiveAnalyzer) is also scheduled on this exact same sequence. Consequently, the timer is blocked by thepbkdf2loop and cannot interrupt the execution. - Browser Fail-Open Behavior: While the utility process is stalled, the browser-side 7-second timeout (
kDownloadRequestTimeoutMs) expires. This cancels the Safe Browsing request with a result ofDownloadCheckResult::UNKNOWN. Because.rarfiles do not have an explicitDANGEROUSlevel in thedownload_file_types.asciipbconfiguration,ChromeDownloadManagerDelegatedefaultsUNKNOWNresults toDOWNLOAD_DANGER_TYPE_NOT_DANGEROUS. The download completes with no security warning.
Suggested Attacker Steps
(Note: These are suggested steps based on code analysis, as our tooling agent does not currently have the ability to run code to produce a working proof-of-concept.)
- Create a malicious RAR5 archive containing multiple dummy encrypted entries.
- For each encrypted dummy entry, modify the
HEAD_CRYPTblock to set theLg2Countfield to24and provide a unique salt (to bypass the unrar KDF cache). - Append a known-malicious, unencrypted payload (e.g., a malware executable) as the final entry in the archive.
- Host the archive and induce a victim to download it in Chrome with Standard Safe Browsing enabled.
- The utility process will hang processing the dummy entries, the browser’s 7-second timeout will expire, and the malicious payload will be saved to disk without any warnings.
Suggested Fix
- Skip Extraction for Encrypted Files: In
third_party/unrar/google/unrar_wrapper.cc, checkarchive_->FileHead.Encryptedbefore callingextractor_->ExtractCurrentFile. If the file is encrypted and no valid password was provided by the user, skip the extraction attempt entirely. This avoids triggering the expensive PBKDF2 calculation for unknown passwords. - Isolate Watchdog Timer: Modify
SafeArchiveAnalyzerto run itstimeout_timer_on a separate sequence or thread so that it can effectively terminate the analysis even if the main utility thread is blocked in a synchronous loop.
Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b
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. And please feel free to reach out to me directly if you have concerns or feedback on the project.