Low watch CVSS 4.3 chrome Input Validation 🔧 Commit mapped

Overview

Low
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper input validation in Extensions
DescriptionImproper input validation in Extensions
ComponentExtensions
Sub-componentCrx
Bug ClassInput Validation
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker538715523
Fix commitfac0b6893477 (chromium/src) +59/-2
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N
CISA KEVNot listed
CreditedJeong Woo Lee (@eclipse07077)
Reported2026-07-24 (per vendor advisory)
Fix landed2026-07-31 (public git)
Disclosed2026-09-08
Patch gap39 days — fix public in source before the advisory

Background

CRX3
Chrome’s extension package format; components/crx_file/crx_verifier.cc verifies the CRX header and signatures before the payload (a ZIP archive) is trusted and unzipped.
ZIP EOCD tokens
signature markers used by ZIP parsers to locate the archive directory: EOCD (PK\x05\x06), EOCD64 locator (PK\x06\x07), and the Zip64 EOCD64 record (PK\x06\x06).
`header_bytes`
the CRX3 protobuf header region that precedes the ZIP payload; if it contains a ZIP end-of-central-directory token, a ZIP parser may key off the wrong location, causing verified content and actually-unzipped content to diverge.
`kDisallowEocdRecord64TokensInCrx`
a BASE_FEATURE added default-enabled as a killswitch so the new check can be disabled without a code roll if needed.

Root Cause Analysis

crx_verifier.cc scanned the CRX header for ZIP end-of-central-directory markers to prevent a mismatch between the bytes that are cryptographically verified and the bytes a ZIP parser would actually treat as the archive directory, but it only searched for kEocd (PK\x05\x06) and kEocd64 (PK\x06\x07). It did not search for the Zip64 EOCD64 record token PK\x06\x06, so a crafted CRX could embed that token inside the header region and still pass verification. Because a ZIP/Zip64 parser can locate the central directory from an EOCD64 record, this allowed the header to influence how the archive is later interpreted, breaking the invariant that the signed/verified region cannot contain a token that steers unzipping.

The fix adds the constant kEocd64Record ({'P','K',0x06,0x06}) and, gated behind the default-enabled feature kDisallowEocdRecord64TokensInCrx, returns VerifierResult::ERROR_HEADER_INVALID when std::ranges::search(header_bytes, kEocd64Record) finds the token, closing the gap left by the two existing checks. This tightens verification: the header now rejects a third class of ZIP token that was previously accepted.

Key insight
The single mistake was an incomplete denylist — the header scan rejected EOCD and the EOCD64 locator but omitted the Zip64 EOCD64 record (PK\x06\x06) token — and the fix adds the missing token check (behind a killswitch feature) so all three markers are rejected.

Attack Path

  1. Craft a malicious CRX An attacker builds a CRX3 file and writes the bytes PK\x06\x06 into the header region (the regression test places them at offset 12, the start of header_bytes).
  2. Pass pre-patch verification Because the old check only searched for kEocd and kEocd64, the embedded EOCD64 record token was not detected and the header was accepted rather than returning ERROR_HEADER_INVALID.
  3. Steer ZIP interpretation The EOCD64 record token in the verified header could cause a ZIP/Zip64 parser to locate the central directory such that the unzipped content differs from what was verified (a verify/parse mismatch).
  4. Deliver mismatched payload Any concrete divergence between verified and extracted extension content, and downstream consequences of loading it, are not demonstrated by the diff (inferred).

Impact Assessment

An attacker who can get a crafted CRX processed could embed a Zip64 EOCD64 record token in the verified header that a ZIP parser may act on, creating a potential mismatch between verified and unzipped content in the extension-verification path. The advisory classifies this as improper input validation in Extensions and does not report exploitation in the wild; the fix is deployed cautiously behind a default-enabled killswitch feature. Any escalation to loading unverified extension content or code execution is (inferred) and not shown by the patch.

Changed Functions

FunctionChangeNotes
TEST_F
components/crx_file/crx_verifier_unittest.cc
modified

Files Changed

  • components/crx_file/crx_verifier_unittest.cc
  • components/crx_file/crx_verifier.cc

Audit Directions

  • Incomplete token/marker denylists
    When code denies a set of format sentinels (magic bytes, ZIP markers, BOMs), enumerate the full specification and confirm every variant is covered; here EOCD64 record (PK\x06\x06) was missing beside EOCD and the EOCD64 locator.
  • Verified-region vs parsed-region mismatch
    Audit other verify-then-parse pipelines for cases where the verified bytes can embed content that changes how a later parser interprets the payload (signature-region confusion).
  • Feature-gated security checks
    Track killswitch-gated checks like kDisallowEocdRecord64TokensInCrx; ensure they are not disabled prematurely and that the check is folded permanently in when the flag is retired (noted for October 2026).
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.