Chrome · Extensions
CVE-2026-87469
Logic Error in Extensions
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fcomponents/crx_file/crx_verifier_unittest.cc |
modified |
Files Changed
components/crx_file/crx_verifier.cccomponents/crx_file/crx_verifier_unittest.cc
Patch
From fac0b6893477da1c69a5fd70b4a18ef4bd8a4a0f Mon Sep 17 00:00:00 2001 From: Devlin Cronin <[email protected]> Date: Fri, 31 Jul 2026 15:51:20 -0700 Subject: [PATCH] [Crx] Disallow EOCD64 Record tokens in crx file headers Just as we disallow EOCD and EOCD64 tokens, we should also disallow EOCD64 record tokens. Guard against these, behind a default-enabled feature flag (out of an abundance of caution), and add a regression test. Bug: 538715523 Change-Id: Ic5a5ac5e93412e42d3d0c32cb1e07cdcf20f552b Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8176148 Commit-Queue: Devlin Cronin <[email protected]> Reviewed-by: Sorin Jianu <[email protected]> Cr-Commit-Position: refs/heads/main@{#1672146} --- diff --git a/components/crx_file/crx_verifier.cc b/components/crx_file/crx_verifier.cc index 77a54cf..2b91e49 100644 --- a/components/crx_file/crx_verifier.cc +++ b/components/crx_file/crx_verifier.cc @@ -5,6 +5,7 @@ #include "components/crx_file/crx_verifier.h" #include <algorithm> +#include <array> #include <climits> #include <cstring> #include <iterator> @@ -14,6 +15,8 @@ #include <utility> #include "base/base64.h" +#include "base/feature.h" +#include "base/feature_list.h" #include "base/files/file.h" #include "base/files/file_path.h" #include "base/functional/bind.h" @@ -35,6 +38,11 @@ using KeyHash = std::array<uint8_t, crypto::hash::kSha256Size>; +// A feature to block EOCD64 record tokens in CRX files, only here as a +// killswitch. This can be removed in October 2026. +BASE_FEATURE(kDisallowEocdRecord64TokensInCrx, + base::FEATURE_ENABLED_BY_DEFAULT); + // The SHA256 hash of the DER SPKI "ecdsa_2017_public" Crx3 key. constexpr KeyHash kPublisherKeyHash = { 0x61, 0xf7, 0xf2, 0xa6, 0xbf, 0xcf, 0x74, 0xcd, 0x0b, 0xc1, 0xfe, @@ -49,6 +57,7 @@ constexpr auto kEocd = std::to_array<uint8_t>({'P', 'K', 0x05, 0x06}); constexpr auto kEocd64 = std::to_array<uint8_t>({'P', 'K', 0x06, 0x07}); +constexpr auto kEocd64Record = std::to_array<uint8_t>({'P', 'K', 0x06, 0x06}); using VerifierCollection = std::vector<std::unique_ptr<crypto::sign::Verifier>>; using RepeatedProof = ::google::protobuf::RepeatedPtrField<AsymmetricKeyProof>; @@ -122,12 +131,19 @@ return VerifierResult::ERROR_HEADER_INVALID; } - // If the header contains a ZIP EOCD or EOCD64 token, unzipping may not work - // correctly. + // If the header contains a ZIP EOCD, EOCD64, or EOCD64 record token, + // unzipping may not work correctly. if (std::ranges::search(header_bytes, kEocd) || std::ranges::search(header_bytes, kEocd64)) { return VerifierResult::ERROR_HEADER_INVALID; } + // Out of an abundance of caution, we gate the EOCD64 record token on a + // base::Feature. The feature check can be removed (and this can be folded + // into the if-statement above) in October 2026. + if (base::FeatureList::IsEnabled(kDisallowEocdRecord64TokensInCrx) && + std::ranges::search(header_bytes, kEocd64Record)) { + return VerifierResult::ERROR_HEADER_INVALID; + } CrxFileHeader header; if (!header.ParseFromArray(header_bytes.data(), header_size)) { diff --git a/components/crx_file/crx_verifier_unittest.cc b/components/crx_file/crx_verifier_unittest.cc index e47f963..8b5bba2 100644 --- a/components/crx_file/crx_verifier_unittest.cc +++ b/components/crx_file/crx_verifier_unittest.cc @@ -3,8 +3,14 @@ // found in the LICENSE file. #include "components/crx_file/crx_verifier.h" + +#include <string> +#include <vector> + #include "base/base_paths.h" #include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/files/scoped_temp_dir.h" #include "base/path_service.h" #include "base/strings/string_number_conversions.h" #include "testing/gtest/include/gtest/gtest.h" @@ -262,4 +268,39 @@ EXPECT_TRUE(compressed_verified_contents.empty()); } +// Tests that we properly reject a crx file that includes an EOCD64 Record +// token. Regression test for https://crbug.com/538715523. +TEST_F(CrxVerifierTest, RejectsEocd64RecordInHeader) { + const std::vector<std::vector<uint8_t>> keys; + const std::vector<uint8_t> hash; + std::string public_key = "UNSET"; + std::string crx_id = "UNSET"; + + std::string crx_contents; + ASSERT_TRUE(base::ReadFileToString(TestFile("valid_no_publisher.crx3"), + &crx_contents)); + + base::ScopedTempDir temp_dir; + ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); + + // Insert PK\x06\x06 (Zip64 EOCD Record) into header_bytes (starts at offset + // 12). + std::string crx_with_eocd64_record = crx_contents; + ASSERT_GT(crx_with_eocd64_record.size(), 20u); + crx_with_eocd64_record[12] = 'P'; + crx_with_eocd64_record[13] = 'K'; + crx_with_eocd64_record[14] = 0x06; + crx_with_eocd64_record[15] = 0x06; + + base::FilePath test_file = + temp_dir.GetPath().AppendASCII("eocd64record.crx3"); + ASSERT_TRUE(base::WriteFile(test_file, crx_with_eocd64_record)); + + EXPECT_EQ(VerifierResult::ERROR_HEADER_INVALID, + Verify(test_file, VerifierFormat::CRX3, keys, hash, &public_key, + &crx_id, /*compressed_verified_contents=*/nullptr)); + EXPECT_EQ("UNSET", crx_id); + EXPECT_EQ("UNSET", public_key); +} + } // namespace crx_file
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/components/crx_file/crx_verifier_unittest.cc b/components/crx_file/crx_verifier_unittest.cc
index e47f963..8b5bba2 100644
--- a/components/crx_file/crx_verifier_unittest.cc
+++ b/components/crx_file/crx_verifier_unittest.cc
@@ -3,8 +3,14 @@
// found in the LICENSE file.
#include "components/crx_file/crx_verifier.h"
+
+#include <string>
+#include <vector>
+
#include "base/base_paths.h"
#include "base/files/file_path.h"
+#include "base/files/file_util.h"
+#include "base/files/scoped_temp_dir.h"
#include "base/path_service.h"
#include "base/strings/string_number_conversions.h"
#include "testing/gtest/include/gtest/gtest.h"
@@ -262,4 +268,39 @@
EXPECT_TRUE(compressed_verified_contents.empty());
}
+// Tests that we properly reject a crx file that includes an EOCD64 Record
+// token. Regression test for https://crbug.com/538715523.
+TEST_F(CrxVerifierTest, RejectsEocd64RecordInHeader) {
+ const std::vector<std::vector<uint8_t>> keys;
+ const std::vector<uint8_t> hash;
+ std::string public_key = "UNSET";
+ std::string crx_id = "UNSET";
+
+ std::string crx_contents;
+ ASSERT_TRUE(base::ReadFileToString(TestFile("valid_no_publisher.crx3"),
+ &crx_contents));
+
+ base::ScopedTempDir temp_dir;
+ ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
+
+ // Insert PK\x06\x06 (Zip64 EOCD Record) into header_bytes (starts at offset
+ // 12).
+ std::string crx_with_eocd64_record = crx_contents;
+ ASSERT_GT(crx_with_eocd64_record.size(), 20u);
+ crx_with_eocd64_record[12] = 'P';
+ crx_with_eocd64_record[13] = 'K';
+ crx_with_eocd64_record[14] = 0x06;
+ crx_with_eocd64_record[15] = 0x06;
+
+ base::FilePath test_file =
+ temp_dir.GetPath().AppendASCII("eocd64record.crx3");
+ ASSERT_TRUE(base::WriteFile(test_file, crx_with_eocd64_record));
+
+ EXPECT_EQ(VerifierResult::ERROR_HEADER_INVALID,
+ Verify(test_file, VerifierFormat::CRX3, keys, hash, &public_key,
+ &crx_id, /*compressed_verified_contents=*/nullptr));
+ EXPECT_EQ("UNSET", crx_id);
+ EXPECT_EQ("UNSET", public_key);
+}
+
} // namespace crx_file
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