Chrome · Safebrowsing
CVE-2026-87656
Logic Error in Safebrowsing
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fchrome/services/file_util/public/cpp/sandboxed_rar_analyzer_unittest.cc |
modified | |
CryptDatathird_party/unrar/patches/chromium_changes.patch |
modified |
Files Changed
chrome/services/file_util/public/cpp/sandboxed_rar_analyzer_unittest.ccchrome/test/data/safe_browsing/rar/v4_many_encrypted.rarthird_party/unrar/patches/chromium_changes.patch
Patch
From 09845d8ce5864c30606117013d0a10859a6f5a58 Mon Sep 17 00:00:00 2001 From: Eriko Kurimoto <[email protected]> Date: Sun, 09 Aug 2026 20:06:39 -0700 Subject: [PATCH] [unrar] Bound the number of key derivations per archive UnRAR caches at most four derived keys per CryptData instance, keyed by (password, salt). RAR 2.9/3.x file headers and RAR 5.x encryption records each carry a per-entry salt, so an archive that uses a fresh salt for every entry forces a full SHA-1 or PBKDF2 key derivation per entry, regardless of whether the supplied password is correct. For RAR 2.9/3.x the per-key cost is fixed at 262144 SHA-1 rounds. Cap the number of distinct keys derived per CryptData instance at CRYPT_KDF_CACHE_MISS_MAX (16). SetKey30 now returns bool, and SetCryptKeys propagates the result, mirroring the existing SetKey50 behaviour. The ReadHeader15 RAR3 header-decryption call now checks the result in the same way the RAR5 path already does. Header metadata is parsed before any key derivation, so all entries are still enumerated and reported as encrypted. Add a SandboxedRarAnalyzerTest covering a RAR 4.x archive with 500 distinct-salt encrypted entries to exercise the new path. Bug: 513245072 Change-Id: Ie4132eac029d32d16c615bd5b75b32c3bf4e3662 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8158623 Reviewed-by: Yaw Frempong <[email protected]> Reviewed-by: Daniel Rubery <[email protected]> SLSA-Policy-Verified: SLSA Policy Verification Service <[email protected]> Commit-Queue: Eriko Kurimoto <[email protected]> Cr-Commit-Position: refs/heads/main@{#1676243} --- 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 15c3c5e..326683e 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 @@ -330,6 +330,22 @@ EXPECT_TRUE(results.archived_archive_filenames.empty()); } +TEST_F(SandboxedRarAnalyzerTest, AnalyzeRar4WithManyEncryptedEntries) { + // v4_many_encrypted.rar is a RAR 4.x archive containing 500 zero-length + // encrypted entries, each with a distinct salt. Analysis should enumerate + // every entry without spending unbounded time on key derivation. + base::FilePath path; + ASSERT_NO_FATAL_FAILURE(path = GetFilePath("v4_many_encrypted.rar")); + + safe_browsing::ArchiveAnalyzerResults results; + AnalyzeFile(path, &results); + + ASSERT_TRUE(results.success); + EXPECT_TRUE(results.has_executable); + EXPECT_EQ(500, results.archived_binary.size()); + EXPECT_TRUE(results.encryption_info.is_encrypted); +} + TEST_F(SandboxedRarAnalyzerTest, AnalyzeRarContainingExecutable) { // Can detect when .rar contains executable files. // has_exe.rar contains 1 file: signed.exe diff --git a/chrome/test/data/safe_browsing/rar/v4_many_encrypted.rar b/chrome/test/data/safe_browsing/rar/v4_many_encrypted.rar new file mode 100644 index 0000000..bfdde134 --- /dev/null +++ b/chrome/test/data/safe_browsing/rar/v4_many_encrypted.rar Binary files differ diff --git a/third_party/unrar/patches/chromium_changes.patch b/third_party/unrar/patches/chromium_changes.patch index 88114b1..c5e6063 100644 --- a/third_party/unrar/patches/chromium_changes.patch +++ b/third_party/unrar/patches/chromium_changes.patch @@ -42,6 +42,23 @@ #ifdef USE_QOPEN bool Open(const std::wstring &Name,uint Mode=FMF_READ) override; int Read(void *Data,size_t Size) override; +diff --git c/third_party/unrar/src/arcread.cpp w/third_party/unrar/src/arcread.cpp +index 7cfa30bf6f40b..204023fb52ed2 100644 +--- c/third_party/unrar/src/arcread.cpp ++++ w/third_party/unrar/src/arcread.cpp +@@ -160,7 +160,11 @@ size_t Archive::ReadHeader15() + UnexpEndArcMsg(); + return 0; + } +- HeadersCrypt.SetCryptKeys(false,CRYPT_RAR30,&Cmd->Password,Salt,NULL,0,NULL,NULL); ++ if (!HeadersCrypt.SetCryptKeys(false,CRYPT_RAR30,&Cmd->Password,Salt,NULL,0,NULL,NULL)) ++ { ++ FailedHeaderDecryption=true; ++ return 0; ++ } + Raw.SetCrypt(&HeadersCrypt); + #endif + } diff --git c/third_party/unrar/src/blake2s.hpp w/third_party/unrar/src/blake2s.hpp index 90b7885fde18a..4d30fe8685745 100644 --- c/third_party/unrar/src/blake2s.hpp @@ -99,11 +116,34 @@ } +diff --git c/third_party/unrar/src/crypt.cpp w/third_party/unrar/src/crypt.cpp +index 9c754ffe404d8..dbc1fb0c3cef3 100644 +--- c/third_party/unrar/src/crypt.cpp ++++ w/third_party/unrar/src/crypt.cpp +@@ -13,6 +13,9 @@ CryptData::CryptData() + Method=CRYPT_NONE; + KDF3CachePos=0; + KDF5CachePos=0; ++#if defined(CHROMIUM_UNRAR) ++ KDFCacheMisses=0; ++#endif + memset(CRCTab,0,sizeof(CRCTab)); + } + +@@ -83,7 +86,7 @@ bool CryptData::SetCryptKeys(bool Encrypt,CRYPT_METHOD Method, + break; + #endif + case CRYPT_RAR30: +- SetKey30(Encrypt,Password,PwdW,Salt); ++ Success=SetKey30(Encrypt,Password,PwdW,Salt); + break; + case CRYPT_RAR50: + Success=SetKey50(Encrypt,Password,PwdW,Salt,InitV,Lg2Cnt,HashKey,PswCheck); diff --git c/third_party/unrar/src/crypt.hpp w/third_party/unrar/src/crypt.hpp -index 286c84807c3d5..6d100cdfe9536 100644 +index 286c84807c3d5..cc5aa4d781a1f 100644 --- c/third_party/unrar/src/crypt.hpp +++ w/third_party/unrar/src/crypt.hpp -@@ -17,9 +17,16 @@ enum CRYPT_METHOD { +@@ -17,9 +17,23 @@ enum CRYPT_METHOD { #define CRYPT_BLOCK_MASK (CRYPT_BLOCK_SIZE-1) // 0xf #define CRYPT5_KDF_LG2_COUNT 15 // LOG2 of PDKDF2 iteration count. @@ -114,12 +154,82 @@ #define CRYPT5_KDF_LG2_COUNT_MAX 24 // LOG2 of maximum accepted iteration count. +#endif + ++#if defined(CHROMIUM_UNRAR) ++// Maximum number of distinct keys to derive per CryptData instance. Archives ++// using more salts than this will have remaining encrypted data treated as ++// undecryptable, while still allowing header metadata to be enumerated. ++#define CRYPT_KDF_CACHE_MISS_MAX 16 ++#endif ++ #define CRYPT_VERSION 0 // Supported encryption version. +static_assert(CRYPT5_KDF_LG2_COUNT <= CRYPT5_KDF_LG2_COUNT_MAX); class CryptData { +@@ -79,7 +93,7 @@ class CryptData + void EncryptBlock20(byte *Buf); + void DecryptBlock20(byte *Buf); + +- void SetKey30(bool Encrypt,SecPassword *Password,const wchar *PwdW,const byte *Salt); ++ bool SetKey30(bool Encrypt,SecPassword *Password,const wchar *PwdW,const byte *Salt); + bool SetKey50(bool Encrypt,SecPassword *Password,const wchar *PwdW,const byte *Salt,const byte *InitV,uint Lg2Cnt,byte *HashKey,byte *PswCheck); + + KDF3CacheItem KDF3Cache[4]; +@@ -88,6 +102,10 @@ class CryptData + KDF5CacheItem KDF5Cache[4]; + uint KDF5CachePos; + ++#if defined(CHROMIUM_UNRAR) ++ uint KDFCacheMisses; ++#endif ++ + CRYPT_METHOD Method; + + Rijndael rin; +diff --git c/third_party/unrar/src/crypt3.cpp w/third_party/unrar/src/crypt3.cpp +index e6e3a82cbb23a..458cc33d7acf4 100644 +--- c/third_party/unrar/src/crypt3.cpp ++++ w/third_party/unrar/src/crypt3.cpp +@@ -1,4 +1,4 @@ +-void CryptData::SetKey30(bool Encrypt,SecPassword *Password,const wchar *PwdW,const byte *Salt) ++bool CryptData::SetKey30(bool Encrypt,SecPassword *Password,const wchar *PwdW,const byte *Salt) + { + byte AESKey[16],AESInit[16]; + +@@ -17,6 +17,10 @@ void CryptData::SetKey30(bool Encrypt,SecPassword *Password,const wchar *PwdW,co + + if (!Cached) + { ++#if defined(CHROMIUM_UNRAR) ++ if (KDFCacheMisses++>=CRYPT_KDF_CACHE_MISS_MAX) ++ return false; ++#endif + byte RawPsw[2*MAXPASSWORD+SIZE_SALT30]; + size_t PswLength=wcslen(PwdW); + size_t RawLength=2*PswLength; +@@ -65,5 +69,6 @@ void CryptData::SetKey30(bool Encrypt,SecPassword *Password,const wchar *PwdW,co + rin.Init(Encrypt, AESKey, 128, AESInit); + cleandata(AESKey,sizeof(AESKey)); + cleandata(AESInit,sizeof(AESInit)); ++ return true; + } + +diff --git c/third_party/unrar/src/crypt5.cpp w/third_party/unrar/src/crypt5.cpp +index 2183c8f0814b5..ed2dfc966436a 100644 +--- c/third_party/unrar/src/crypt5.cpp ++++ w/third_party/unrar/src/crypt5.cpp
Loading diff…
Regression Test / PoC
shipped with the fix
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 15c3c5e..326683e 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
@@ -330,6 +330,22 @@
EXPECT_TRUE(results.archived_archive_filenames.empty());
}
+TEST_F(SandboxedRarAnalyzerTest, AnalyzeRar4WithManyEncryptedEntries) {
+ // v4_many_encrypted.rar is a RAR 4.x archive containing 500 zero-length
+ // encrypted entries, each with a distinct salt. Analysis should enumerate
+ // every entry without spending unbounded time on key derivation.
+ base::FilePath path;
+ ASSERT_NO_FATAL_FAILURE(path = GetFilePath("v4_many_encrypted.rar"));
+
+ safe_browsing::ArchiveAnalyzerResults results;
+ AnalyzeFile(path, &results);
+
+ ASSERT_TRUE(results.success);
+ EXPECT_TRUE(results.has_executable);
+ EXPECT_EQ(500, results.archived_binary.size());
+ EXPECT_TRUE(results.encryption_info.is_encrypted);
+}
+
TEST_F(SandboxedRarAnalyzerTest, AnalyzeRarContainingExecutable) {
// Can detect when .rar contains executable files.
// has_exe.rar contains 1 file: signed.exe
diff --git a/chrome/test/data/safe_browsing/rar/v4_many_encrypted.rar b/chrome/test/data/safe_browsing/rar/v4_many_encrypted.rar
new file mode 100644
index 0000000..bfdde134
--- /dev/null
+++ b/chrome/test/data/safe_browsing/rar/v4_many_encrypted.rar
Binary files differ
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page