CVE-2026-11276
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcomponents/media_router/common/providers/cast/certificate/cast_cert_validator.cc |
modified | |
MockCastCRLcomponents/media_router/common/providers/cast/certificate/cast_cert_validator_unittest.cc |
modified | |
TESTcomponents/media_router/common/providers/cast/certificate/cast_cert_validator_unittest.cc |
modified |
Files Changed
components/media_router/common/providers/cast/certificate/cast_cert_validator.cccomponents/media_router/common/providers/cast/certificate/cast_cert_validator.hcomponents/media_router/common/providers/cast/certificate/cast_cert_validator_unittest.cc
Patch
From 957af2e23634532bc972fa84ca2bfa59a6b2f84a Mon Sep 17 00:00:00 2001
From: Muyao Xu <muyaoxu@google.com>
Date: Thu, 16 Apr 2026 15:51:23 -0700
Subject: [PATCH] [Cast] Fix potential revocation bypass via stale CRL
The fallback CRL was not checked if the device provided its own CRL,
allowing an attacker to bypass revocation by providing a stale but
time-valid CRL.
This change ensures that if both a device CRL and a fallback CRL are
available, the fallback CRL is also checked. If either CRL revokes
the certificate, it is rejected.
Bug: 501780338
Change-Id: I98846d488ab14703a9f8364597db958233f9c8b3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7762964
Commit-Queue: Muyao Xu <muyaoxu@google.com>
Reviewed-by: Jordan Bayles <jophba@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1616168}
---
diff --git a/components/media_router/common/providers/cast/certificate/cast_cert_validator.cc b/components/media_router/common/providers/cast/certificate/cast_cert_validator.cc
index bfb27a21..621c6b5 100644
--- a/components/media_router/common/providers/cast/certificate/cast_cert_validator.cc
+++ b/components/media_router/common/providers/cast/certificate/cast_cert_validator.cc
@@ -411,17 +411,20 @@
return CastCertError::ERR_CERTS_RESTRICTIONS;
}
- if (!crl && (crl_policy == CRLPolicy::CRL_REQUIRED_WITH_FALLBACK ||
- crl_policy == CRLPolicy::CRL_OPTIONAL_WITH_FALLBACK)) {
- if (!fallback_crl) {
+ if (crl_policy == CRLPolicy::CRL_REQUIRED_WITH_FALLBACK ||
+ crl_policy == CRLPolicy::CRL_OPTIONAL_WITH_FALLBACK) {
+ if (fallback_crl) {
+ if (!fallback_crl->CheckRevocation(result.GetBestValidPath()->certs,
+ time)) {
+ return CastCertError::ERR_CERTS_REVOKED_BY_FALLBACK_CRL;
+ }
+ } else if (!crl) {
return CastCertError::ERR_FALLBACK_CRL_INVALID;
}
- if (!fallback_crl->CheckRevocation(result.GetBestValidPath()->certs,
- time)) {
- return CastCertError::ERR_CERTS_REVOKED_BY_FALLBACK_CRL;
+ if (!crl) {
+ return CastCertError::OK_FALLBACK_CRL;
}
- return CastCertError::OK_FALLBACK_CRL;
}
// Check for revocation.
diff --git a/components/media_router/common/providers/cast/certificate/cast_cert_validator.h b/components/media_router/common/providers/cast/certificate/cast_cert_validator.h
index edf99e3..e2d18c63 100644
--- a/components/media_router/common/providers/cast/certificate/cast_cert_validator.h
+++ b/components/media_router/common/providers/cast/certificate/cast_cert_validator.h
@@ -32,8 +32,10 @@
};
enum class CRLPolicy {
- // Revocation is checked if a CRL is provided. If CRL is not provided,
- // revocation is checked by fallback CRL.
+ // Revocation is checked if a CRL is provided. The fallback CRL is also
+ // checked if provided, to prevent a stale CRL from shadowing a revoking
+ // fallback CRL. If CRL is not provided, revocation is checked by fallback
+ // CRL.
//
// DEPRECATED. A CRL is always required.
//
@@ -47,8 +49,9 @@
// TODO(crbug.com/411575751): Remove this policy.
CRL_OPTIONAL,
- // Revocation is always checked. If CRL is not provided, revocation is checked
- // by fallback CRL.
+ // Revocation is always checked. The fallback CRL is also checked if provided,
+ // to prevent a stale CRL from shadowing a revoking fallback CRL. If CRL is
+ // not provided, revocation is checked by fallback CRL.
CRL_REQUIRED_WITH_FALLBACK,
// Revocation is always checked. A missing CRL results in failure.
diff --git a/components/media_router/common/providers/cast/certificate/cast_cert_validator_unittest.cc b/components/media_router/common/providers/cast/certificate/cast_cert_validator_unittest.cc
index 3d1a2fd..c87068a 100644
--- a/components/media_router/common/providers/cast/certificate/cast_cert_validator_unittest.cc
+++ b/components/media_router/common/providers/cast/certificate/cast_cert_validator_unittest.cc
@@ -11,6 +11,7 @@
#include "base/time/time.h"
#include "components/media_router/common/providers/cast/certificate/cast_cert_reader.h"
#include "components/media_router/common/providers/cast/certificate/cast_cert_test_helpers.h"
+#include "components/media_router/common/providers/cast/certificate/cast_crl.h"
#include "components/media_router/common/providers/cast/certificate/switches.h"
#include "net/cert/x509_util.h"
#include "testing/gtest/include/gtest/gtest.h"
@@ -23,6 +24,18 @@
namespace {
+class MockCastCRL : public CastCRL {
+ public:
+ explicit MockCastCRL(bool revoked) : revoked_(revoked) {}
+ bool CheckRevocation(const bssl::ParsedCertificateList& trusted_chain,
+ const base::Time& time) const override {
+ return !revoked_;
+ }
+
+ private:
+ bool revoked_;
+};
+
// Creates an std::string given a uint8_t array.
template <size_t N>
std::string CreateString(const uint8_t (&data)[N]) {
@@ -634,6 +647,63 @@
"signeddata/rsa2048_device_cert_data.pem");
}
+TEST(CastCertValidatorRevocationTest, StaleDeviceCrlBypassesFallbackCrl) {
+ // Load a valid certificate chain.
+ auto certs = ReadCertificateChainFromFile(
+ testing::GetCastCertificatesSubDirectory().AppendASCII(
+ "chromecast_gen1.pem"));
+ ASSERT_FALSE(certs.empty());
+
+ // Setup trust store.
+ bssl::CertErrors errors;
+ std::shared_ptr<const bssl::ParsedCertificate> root =
+ bssl::ParsedCertificate::Create(
+ net::x509_util::CreateCryptoBuffer(certs.back()), {}, &errors);
+ ASSERT_TRUE(root) << errors.ToDebugString();
+ certs.pop_back();
+
+ bssl::TrustStoreInMemory trust_store;
+ trust_store.AddTrustAnchorWithConstraints(std::move(root));
+
+ std::unique_ptr<CertVerificationContext> context;
+ CastDeviceCertPolicy policy;
+ base::Time time = AprilFirst2016();
+
+ // 1. No device CRL, fallback CRL revokes.
+ // Expect: ERR_CERTS_REVOKED_BY_FALLBACK_CRL
+ {
+ MockCastCRL fallback_crl(true); // Revoked
+ CastCertError result = VerifyDeviceCertUsingCustomTrustStore(
+ certs, time, &context, &policy, nullptr, &fallback_crl,
+ CRLPolicy::CRL_REQUIRED_WITH_FALLBACK, &trust_store);
+ EXPECT_EQ(CastCertError::ERR_CERTS_REVOKED_BY_FALLBACK_CRL, result);
+ }
+
+ // 2. Device CRL (not revoking), fallback CRL (revoking).
+ // This is the VULNERABILITY: device CRL shadows fallback CRL.
+ // Expect: ERR_CERTS_REVOKED_BY_FALLBACK_CRL
+ {
+ MockCastCRL device_crl(false); // Not revoked (stale CRL)
+ MockCastCRL fallback_crl(true); // Revoked
+ CastCertError result = VerifyDeviceCertUsingCustomTrustStore(
+ certs, time, &context, &policy, &device_crl, &fallback_crl,
+ CRLPolicy::CRL_REQUIRED_WITH_FALLBACK, &trust_store);
+
+ EXPECT_EQ(CastCertError::ERR_CERTS_REVOKED_BY_FALLBACK_CRL, result);
+ }
+
+ // 3. Valid device CRL, no fallback CRL (e.g. built-in fallback CRL expired).
+ // Expect: OK
+ {
+ MockCastCRL device_crl(false); // Not revoked
+ CastCertError result = VerifyDeviceCertUsingCustomTrustStore(
+ certs, time, &context, &policy, &device_crl, nullptr,
+ CRLPolicy::CRL_REQUIRED_WITH_FALLBACK, &trust_store);
+
+ EXPECT_EQ(CastCertError::OK, result);
+ }
+}
+
} // namespace
} // namespace cast_certificate
Regression Test / PoC
diff --git a/components/media_router/common/providers/cast/certificate/cast_cert_validator_unittest.cc b/components/media_router/common/providers/cast/certificate/cast_cert_validator_unittest.cc
index 3d1a2fd..c87068a 100644
--- a/components/media_router/common/providers/cast/certificate/cast_cert_validator_unittest.cc
+++ b/components/media_router/common/providers/cast/certificate/cast_cert_validator_unittest.cc
@@ -11,6 +11,7 @@
#include "base/time/time.h"
#include "components/media_router/common/providers/cast/certificate/cast_cert_reader.h"
#include "components/media_router/common/providers/cast/certificate/cast_cert_test_helpers.h"
+#include "components/media_router/common/providers/cast/certificate/cast_crl.h"
#include "components/media_router/common/providers/cast/certificate/switches.h"
#include "net/cert/x509_util.h"
#include "testing/gtest/include/gtest/gtest.h"
@@ -23,6 +24,18 @@
namespace {
+class MockCastCRL : public CastCRL {
+ public:
+ explicit MockCastCRL(bool revoked) : revoked_(revoked) {}
+ bool CheckRevocation(const bssl::ParsedCertificateList& trusted_chain,
+ const base::Time& time) const override {
+ return !revoked_;
+ }
+
+ private:
+ bool revoked_;
+};
+
// Creates an std::string given a uint8_t array.
template <size_t N>
std::string CreateString(const uint8_t (&data)[N]) {
@@ -634,6 +647,63 @@
"signeddata/rsa2048_device_cert_data.pem");
}
+TEST(CastCertValidatorRevocationTest, StaleDeviceCrlBypassesFallbackCrl) {
+ // Load a valid certificate chain.
+ auto certs = ReadCertificateChainFromFile(
+ testing::GetCastCertificatesSubDirectory().AppendASCII(
+ "chromecast_gen1.pem"));
+ ASSERT_FALSE(certs.empty());
+
+ // Setup trust store.
+ bssl::CertErrors errors;
+ std::shared_ptr<const bssl::ParsedCertificate> root =
+ bssl::ParsedCertificate::Create(
+ net::x509_util::CreateCryptoBuffer(certs.back()), {}, &errors);
+ ASSERT_TRUE(root) << errors.ToDebugString();
+ certs.pop_back();
+
+ bssl::TrustStoreInMemory trust_store;
+ trust_store.AddTrustAnchorWithConstraints(std::move(root));
+
+ std::unique_ptr<CertVerificationContext> context;
+ CastDeviceCertPolicy policy;
+ base::Time time = AprilFirst2016();
+
+ // 1. No device CRL, fallback CRL revokes.
+ // Expect: ERR_CERTS_REVOKED_BY_FALLBACK_CRL
+ {
+ MockCastCRL fallback_crl(true); // Revoked
+ CastCertError result = VerifyDeviceCertUsingCustomTrustStore(
+ certs, time, &context, &policy, nullptr, &fallback_crl,
+ CRLPolicy::CRL_REQUIRED_WITH_FALLBACK, &trust_store);
+ EXPECT_EQ(CastCertError::ERR_CERTS_REVOKED_BY_FALLBACK_CRL, result);
+ }
+
+ // 2. Device CRL (not revoking), fallback CRL (revoking).
+ // This is the VULNERABILITY: device CRL shadows fallback CRL.
+ // Expect: ERR_CERTS_REVOKED_BY_FALLBACK_CRL
+ {
+ MockCastCRL device_crl(false); // Not revoked (stale CRL)
+ MockCastCRL fallback_crl(true); // Revoked
+ CastCertError result = VerifyDeviceCertUsingCustomTrustStore(
+ certs, time, &context, &policy, &device_crl, &fallback_crl,
+ CRLPolicy::CRL_REQUIRED_WITH_FALLBACK, &trust_store);
+
+ EXPECT_EQ(CastCertError::ERR_CERTS_REVOKED_BY_FALLBACK_CRL, result);
+ }
+
+ // 3. Valid device CRL, no fallback CRL (e.g. built-in fallback CRL expired).
+ // Expect: OK
+ {
+ MockCastCRL device_crl(false); // Not revoked
+ CastCertError result = VerifyDeviceCertUsingCustomTrustStore(
+ certs, time, &context, &policy, &device_crl, nullptr,
+ CRLPolicy::CRL_REQUIRED_WITH_FALLBACK, &trust_store);
+
+ EXPECT_EQ(CastCertError::OK, result);
+ }
+}
+
} // namespace
} // namespace cast_certificate
Original Bug Report
Potential revocation bypass via stale CRL in Cast device authentication
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: Chrome’s Cast device authentication skips checking the built-in fallback CRL if the device provides its own validly signed CRL. An attacker possessing a revoked Cast certificate could bypass revocation by providing an older, but still time-valid, CRL that was issued before their certificate was revoked. This could allow a malicious device on the local network to authenticate as a genuine Google Cast receiver.
Affected files:
components/media_router/common/providers/cast/certificate/cast_cert_validator.cccomponents/media_router/common/providers/cast/channel/cast_auth_util.cccomponents/media_router/common/providers/cast/certificate/cast_crl.cc
Estimated timestamp from git blame: 2023-10-19
Description
Chrome’s Cast device authentication mechanism (Cast PKI) ensures that Chrome only communicates with genuine, non-revoked Google Cast hardware. To handle compromised device keys, Chrome relies on Certificate Revocation Lists (CRLs). A fallback CRL (kCastFallbackCRLs) is baked into Chrome to provide up-to-date revocation information even if the device cannot provide a fresh CRL.
There is a potential logic flaw in components/media_router/common/providers/cast/certificate/cast_cert_validator.cc that allows a device-supplied CRL to completely shadow the built-in fallback CRL.
In VerifyDeviceCertUsingCustomTrustStore, the fallback CRL is only checked if the device fails to provide its own CRL:
if (!crl && (crl_policy == CRLPolicy::CRL_REQUIRED_WITH_FALLBACK ||
crl_policy == CRLPolicy::CRL_OPTIONAL_WITH_FALLBACK)) {
if (!fallback_crl) {
return CastCertError::ERR_FALLBACK_CRL_INVALID;
}
if (!fallback_crl->CheckRevocation(result.GetBestValidPath()->certs,
time)) {
return CastCertError::ERR_CERTS_REVOKED_BY_FALLBACK_CRL;
}
return CastCertError::OK_FALLBACK_CRL;
}
// Check for revocation using the device-provided CRL.
if (crl && !crl->CheckRevocation(result.GetBestValidPath()->certs, time)) {
return CastCertError::ERR_CERTS_REVOKED;
}
The issue arises because cast_crl.cc’s validation of the device-provided CRL only verifies the RSA signature and ensures the current time falls within the [not_before, not_after] window. It does not enforce freshness relative to the fallback_crl.
Consequently, an attacker possessing a revoked Cast certificate can present a “stale” CRL. As long as this stale CRL is genuinely signed by Google and its not_after date has not yet passed, VerifyDeviceCertUsingCustomTrustStore will accept it, bypass the fallback_crl check entirely, and fail to detect the revocation.
Potential Exploit Scenario
An attacker could potentially exploit this via the following steps:
- Obtain Compromised Materials: The attacker acquires the private key of a formally revoked Google Cast receiver, alongside a stale, signed
CrlBundleissued prior to the revocation date but still within itsnot_aftervalidity window. - Network Advertisement: The attacker runs a malicious Cast receiver on the same local network as the victim, broadcasting via mDNS (
_googlecast._tcp). - Connection: The victim’s Chrome browser discovers the device and initiates a connection, sending an
AuthChallenge. - Malicious Response: The attacker responds with a
DeviceAuthMessagecontaining the compromised certificate chain, a valid challenge signature, and the staleCrlBundle. - Validation Bypass: Chrome processes the response in
VerifyCredentialsImpl. It parses the stale CRL and the built-in fallback CRL. InVerifyDeviceCertUsingCustomTrustStore, the presence of the valid (but stale) device CRL causes the fallback CRL check to be skipped. Since the stale CRL does not contain the revocation, authentication succeeds.
Impact
A successfully authenticated malicious Cast receiver can intercept media URLs, receive authentication tokens, and capture sensitive screen-mirroring streams intended for a genuine device.
Suggested Fix
The validation logic should be updated to ensure the most recent revocation data is used. If both a device crl and a fallback_crl are available, the validator should compare their issuance times (not_before or not_after) and use the freshest one, or defensively check for revocation against both CRLs before returning OK.
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.