Medium watch ✔︎ verified CVSS 4.3 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper input validation in Variations
DescriptionImproper input validation in Variations
ComponentVariations
Bug ClassInput Validation
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety — factors set by independent review by AI
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.
Independent review by AIConfirmed
Reviewer notes: Citations accurate. Omits commit message note that NetworkTimeTracker already caps uncertainty (60s timeout + 60s skew), bounding achievable skew; report's multi-hour skew and expired-cert impact are unverified and stated without qualification in Impact.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker502082953
Fix commit3b4225c11d5b (chromium/src) +91/-1
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-04-13 (per vendor advisory)
Fix landed2026-07-18 (public git)
Disclosed2026-08-25
Patch gap38 days — fix public in source before the advisory

Background

`CertVerifierServiceFactoryImpl::UpdateNetworkTime`
the cert-verifier-service entry point (services/cert_verifier/cert_verifier_service_factory.cc) that receives a network-time measurement and installs it as the time source used for certificate validity checks.
`TimeTracker` (`proc_params_.time_tracker`)
the custom network-time state the cert verifier uses in place of the system clock when validating certificate dates.
`uncertainty`
the error bound reported with a network-time measurement, expressing how imprecise that time estimate is.
`kMaxUncertaintyForNetworkTime`
the new 45-second ceiling above which a network-time update is rejected.

Root Cause Analysis

Re-analyzed on 2026-10-03 against the actual fix commit 3b4225c11d5b. An earlier analysis was withdrawn because it had been written against a commit that was not the security fix (823df872a358).

CertVerifierServiceFactoryImpl::UpdateNetworkTime receives system_time, system_ticks, current_time, and uncertainty (cert_verifier_service_factory.cc:567-571). Pre-fix, it ignored uncertainty for any decision: a TODO: crbug.com/502082953 - Consider what to do if the uncertainty is high. (cert_verifier_service_factory.cc:572) was followed unconditionally by proc_params_.time_tracker.emplace(system_time, system_ticks, current_time, uncertainty) and UpdateVerifierServices() (cert_verifier_service_factory.cc:573-575). The violated invariant is that certificate validation must not rely on a network time whose error bound is large enough to move “now” across a certificate’s validity window; the pre-fix code installed every measurement regardless of its uncertainty, so an attacker-skewed, high-uncertainty time estimate would still be trusted for date checks.

The fix (commit 3b4225c11d5b5b940f0acbaad1538ee3893e8d72) adds constexpr base::TimeDelta kMaxUncertaintyForNetworkTime = base::Seconds(45) and, in place of the TODO, if (uncertainty > kMaxUncertaintyForNetworkTime) { return; }, so an update whose uncertainty exceeds the threshold is dropped and the prior (e.g. system-clock-based) time is retained. The added unit tests confirm an update at 45s is applied while one at 46s is ignored (falling back to system time, yielding net::ERR_CERT_DATE_INVALID). Note the material shows uncertainty was already a parameter here and was already forwarded into the tracker, which contradicts the report’s claim that uncertainty is “discarded” / hardcoded to zero on this path; the code wins.

Key insight
The cert verifier consumed network time without regard to its stated error bound; the fix rejects any update whose uncertainty exceeds a 45-second ceiling, so a wide-margin (attacker-stretched) time estimate can no longer drive certificate-date validation.

Attack Path

  1. Delay a timed network fetch An on-path attacker lets a time-bearing response’s headers arrive but trickles its body, inflating the measured round-trip and thus the derived network-time uncertainty (reporter’s trigger via a Variations seed fetch; the Variations/latency path is not shown in this commit’s material, (inferred)).
  2. Skew network time into the past The large measured latency shifts the estimated “now” earlier while producing a correspondingly large uncertainty.
  3. Trusted by the cert verifier (pre-fix) UpdateNetworkTime installs that skewed time into proc_params_.time_tracker unconditionally (cert_verifier_service_factory.cc:572-575).
  4. Accept an expired certificate Certificate date checks run against the past-skewed time, so a recently expired certificate for a target origin validates as still-valid, enabling origin impersonation (inferred).

Impact Assessment

An on-path (MITM) attacker who can stretch a timed fetch could push Chrome’s network time into the past with a large uncertainty and, pre-fix, have the certificate verifier trust it, allowing acceptance of a recently expired certificate and impersonation of a target origin. Preconditions are an on-path position and the ability to influence a network-time measurement; the advisory rates this medium severity with no in-the-wild exploitation reported. The fix bounds the exposure to time updates whose uncertainty is at most 45 seconds.

Changed Functions

FunctionChangeNotes
if
services/cert_verifier/cert_verifier_service_factory.cc
modified

Files Changed

  • services/cert_verifier/cert_verifier_service_factory.cc
  • services/cert_verifier/cert_verifier_service_factory_unittest.cc

Audit Directions

  • Unused precision metadata
    Flag code that receives an uncertainty/confidence value but makes no decision based on it (especially a lingering TODO beside a security-relevant emplace/SetHeader/Set*).
  • Network time feeding security checks
    Audit every consumer of network-time state used for certificate or validity-window decisions to confirm it bounds or rejects low-confidence measurements rather than trusting them implicitly.
  • Cross-service invariant drift
    When one service (network time) caps a value and another (cert verifier) consumes it, verify the consumer re-validates independently instead of assuming the producer’s bound.
diff --git a/services/cert_verifier/cert_verifier_service_factory.cc b/services/cert_verifier/cert_verifier_service_factory.cc
index 2b9e4cd..5a58892 100644
--- a/services/cert_verifier/cert_verifier_service_factory.cc
+++ b/services/cert_verifier/cert_verifier_service_factory.cc
@@ -66,6 +66,15 @@
 namespace cert_verifier {
 namespace {
 
+// The maximum uncertainty for network time that is considered acceptable. This
+// value is used to determine whether the network time update should be used to
+// update the CertVerifierService's time tracker.
+//
+// This is derived from the 99th percentile of observed network time tracker
+// uncertainty in June 2026, plus a comfortable margin. It is still well below
+// the maximum uncertainty allowed by the network time service's timeout.
+constexpr base::TimeDelta kMaxUncertaintyForNetworkTime = base::Seconds(45);
+
 internal::CertVerifierServiceImpl* GetNewCertVerifierImpl(
     mojo::PendingReceiver<mojom::CertVerifierService> service_receiver,
     mojo::PendingReceiver<mojom::CertVerifierServiceUpdater> updater_receiver,
@@ -569,7 +578,10 @@
     base::TimeTicks system_ticks,
     base::Time current_time,
     base::TimeDelta uncertainty) {
-  // TODO: crbug.com/502082953 - Consider what to do if the uncertainty is high.
+  // Ignore network time updates having excessive uncertainty.
+  if (uncertainty > kMaxUncertaintyForNetworkTime) {
+    return;
+  }
   proc_params_.time_tracker.emplace(system_time, system_ticks, current_time,
                                     uncertainty);
   UpdateVerifierServices();
diff --git a/services/cert_verifier/cert_verifier_service_factory_unittest.cc b/services/cert_verifier/cert_verifier_service_factory_unittest.cc
index d0d6a71..cba40f3 100644
--- a/services/cert_verifier/cert_verifier_service_factory_unittest.cc
+++ b/services/cert_verifier/cert_verifier_service_factory_unittest.cc
@@ -1891,4 +1891,82 @@
 }
 #endif  // BUILDFLAG(CHROME_ROOT_STORE_SUPPORTED)
 
+TEST_F(CertVerifierServiceFactoryBuiltinVerifierTest,
+       UpdateNetworkTime_Applied) {
+  auto [leaf, root] = net::CertBuilder::CreateSimpleChain2();
+  base::Time now = base::Time::Now();
+  base::TimeTicks ticks_now = base::TimeTicks::Now();
+  // Configure the leaf certificate so it is no longer valid according to the
+  // system time.
+  leaf->SetValidity(now - base::Days(3), now - base::Days(1));
+  leaf->SetSubjectAltName("host.test");
+  net::ScopedTestRoot scoped_test_root(root->GetX509Certificate());
+
+  mojo::Remote<mojom::CertVerifierServiceFactory> cv_service_factory_remote;
+  CertVerifierServiceFactoryImpl cv_service_factory_impl(
+      cv_service_factory_remote.BindNewPipeAndPassReceiver());
+  EnableChromeRootStoreIfOptional(&cv_service_factory_impl);
+
+  // Update the time tracker with uncertainty exactly at the threshold (45s).
+  // This should be applied.
+  cv_service_factory_impl.UpdateNetworkTime(now, ticks_now, now - base::Days(2),
+                                            base::Seconds(45));
+
+  mojo::Remote<mojom::CertVerifierService> cv_service_remote;
+  DummyCVServiceClient cv_service_client;
+  mojom::CertVerifierCreationParamsPtr cv_creation_params =
+      mojom::CertVerifierCreationParams::New();
+
+  cv_service_factory_remote->GetNewCertVerifier(
+      cv_service_remote.BindNewPipeAndPassReceiver(),
+      /*updater=*/mojo::NullReceiver(),
+      cv_service_client.client_.BindNewPipeAndPassRemote(),
+      std::move(cv_creation_params));
+
+  auto [net_error, result] =
+      Verify(cv_service_remote, leaf->GetX509Certificate(), "host.test");
+  EXPECT_THAT(net_error, IsError(net::OK));
+  EXPECT_FALSE(net::IsCertStatusError(result.cert_status));
+}
+
+TEST_F(CertVerifierServiceFactoryBuiltinVerifierTest,
+       UpdateNetworkTime_Ignored) {
+  auto [leaf, root] = net::CertBuilder::CreateSimpleChain2();
+  base::Time now = base::Time::Now();
+  base::TimeTicks ticks_now = base::TimeTicks::Now();
+  // Configure the leaf certificate so it is no longer valid according to the
+  // system time.
+  leaf->SetValidity(now - base::Days(3), now - base::Days(1));
+  leaf->SetSubjectAltName("host.test");
+  net::ScopedTestRoot scoped_test_root(root->GetX509Certificate());
+
+  mojo::Remote<mojom::CertVerifierServiceFactory> cv_service_factory_remote;
+  CertVerifierServiceFactoryImpl cv_service_factory_impl(
+      cv_service_factory_remote.BindNewPipeAndPassReceiver());
+  EnableChromeRootStoreIfOptional(&cv_service_factory_impl);
+
+  // Update the time tracker with uncertainty just above the threshold (46s).
+  // This should be ignored.
+  cv_service_factory_impl.UpdateNetworkTime(now, ticks_now, now - base::Days(2),
+                                            base::Seconds(46));
+
+  mojo::Remote<mojom::CertVerifierService> cv_service_remote;
+  DummyCVServiceClient cv_service_client;
+  mojom::CertVerifierCreationParamsPtr cv_creation_params =
+      mojom::CertVerifierCreationParams::New();
+
+  cv_service_factory_remote->GetNewCertVerifier(
+      cv_service_remote.BindNewPipeAndPassReceiver(),
+      /*updater=*/mojo::NullReceiver(),
+      cv_service_client.client_.BindNewPipeAndPassRemote(),
+      std::move(cv_creation_params));
+
+  auto [net_error, result] =
+      Verify(cv_service_remote, leaf->GetX509Certificate(), "host.test");
+  // Should fail because the network time update was ignored, so it used
+  // system time (which is expired).
+  EXPECT_THAT(net_error, IsError(net::ERR_CERT_DATE_INVALID));
+  EXPECT_TRUE(net::IsCertStatusError(result.cert_status));
+}
+
 }  // namespace cert_verifier
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/services/cert_verifier/cert_verifier_service_factory_unittest.cc b/services/cert_verifier/cert_verifier_service_factory_unittest.cc
index d0d6a71..cba40f3 100644
--- a/services/cert_verifier/cert_verifier_service_factory_unittest.cc
+++ b/services/cert_verifier/cert_verifier_service_factory_unittest.cc
@@ -1891,4 +1891,82 @@
 }
 #endif  // BUILDFLAG(CHROME_ROOT_STORE_SUPPORTED)
 
+TEST_F(CertVerifierServiceFactoryBuiltinVerifierTest,
+       UpdateNetworkTime_Applied) {
+  auto [leaf, root] = net::CertBuilder::CreateSimpleChain2();
+  base::Time now = base::Time::Now();
+  base::TimeTicks ticks_now = base::TimeTicks::Now();
+  // Configure the leaf certificate so it is no longer valid according to the
+  // system time.
+  leaf->SetValidity(now - base::Days(3), now - base::Days(1));
+  leaf->SetSubjectAltName("host.test");
+  net::ScopedTestRoot scoped_test_root(root->GetX509Certificate());
+
+  mojo::Remote<mojom::CertVerifierServiceFactory> cv_service_factory_remote;
+  CertVerifierServiceFactoryImpl cv_service_factory_impl(
+      cv_service_factory_remote.BindNewPipeAndPassReceiver());
+  EnableChromeRootStoreIfOptional(&cv_service_factory_impl);
+
+  // Update the time tracker with uncertainty exactly at the threshold (45s).
+  // This should be applied.
+  cv_service_factory_impl.UpdateNetworkTime(now, ticks_now, now - base::Days(2),
+                                            base::Seconds(45));
+
+  mojo::Remote<mojom::CertVerifierService> cv_service_remote;
+  DummyCVServiceClient cv_service_client;
+  mojom::CertVerifierCreationParamsPtr cv_creation_params =
+      mojom::CertVerifierCreationParams::New();
+
+  cv_service_factory_remote->GetNewCertVerifier(
+      cv_service_remote.BindNewPipeAndPassReceiver(),
+      /*updater=*/mojo::NullReceiver(),
+      cv_service_client.client_.BindNewPipeAndPassRemote(),
+      std::move(cv_creation_params));
+
+  auto [net_error, result] =
+      Verify(cv_service_remote, leaf->GetX509Certificate(), "host.test");
+  EXPECT_THAT(net_error, IsError(net::OK));
+  EXPECT_FALSE(net::IsCertStatusError(result.cert_status));
+}
+
+TEST_F(CertVerifierServiceFactoryBuiltinVerifierTest,
+       UpdateNetworkTime_Ignored) {
+  auto [leaf, root] = net::CertBuilder::CreateSimpleChain2();
+  base::Time now = base::Time::Now();
+  base::TimeTicks ticks_now = base::TimeTicks::Now();
+  // Configure the leaf certificate so it is no longer valid according to the
+  // system time.
+  leaf->SetValidity(now - base::Days(3), now - base::Days(1));
+  leaf->SetSubjectAltName("host.test");
+  net::ScopedTestRoot scoped_test_root(root->GetX509Certificate());
+
+  mojo::Remote<mojom::CertVerifierServiceFactory> cv_service_factory_remote;
+  CertVerifierServiceFactoryImpl cv_service_factory_impl(
+      cv_service_factory_remote.BindNewPipeAndPassReceiver());
+  EnableChromeRootStoreIfOptional(&cv_service_factory_impl);
+
+  // Update the time tracker with uncertainty just above the threshold (46s).
+  // This should be ignored.
+  cv_service_factory_impl.UpdateNetworkTime(now, ticks_now, now - base::Days(2),
+                                            base::Seconds(46));
+
+  mojo::Remote<mojom::CertVerifierService> cv_service_remote;
+  DummyCVServiceClient cv_service_client;
+  mojom::CertVerifierCreationParamsPtr cv_creation_params =
+      mojom::CertVerifierCreationParams::New();
+
+  cv_service_factory_remote->GetNewCertVerifier(
+      cv_service_remote.BindNewPipeAndPassReceiver(),
+      /*updater=*/mojo::NullReceiver(),
+      cv_service_client.client_.BindNewPipeAndPassRemote(),
+      std::move(cv_creation_params));
+
+  auto [net_error, result] =
+      Verify(cv_service_remote, leaf->GetX509Certificate(), "host.test");
+  // Should fail because the network time update was ignored, so it used
+  // system time (which is expired).
+  EXPECT_THAT(net_error, IsError(net::ERR_CERT_DATE_INVALID));
+  EXPECT_TRUE(net::IsCertStatusError(result.cert_status));
+}
+
 }  // namespace cert_verifier
Loading diff…

Original Bug Report

reported by vm...@google.com

Cert validation bypass via NetworkTimeTracker past-skew from delayed Variations Seed body

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: An on-path attacker can skew Chrome’s NetworkTimeTracker into the past by intentionally delaying the HTTP response body of a Variations Seed fetch. The resulting uncertainty value is incorrectly discarded before reaching the CertVerifierService, causing it to fully trust the skewed network time. This potentially allows the attacker to bypass certificate expiration checks and impersonate arbitrary origins using expired certificates.

Affected files:

  • components/variations/service/variations_service.cc
  • chrome/browser/net/cert_verifier_service_time_updater.cc
  • services/cert_verifier/cert_verifier_service_factory.cc
  • net/cert/cert_verify_proc_builtin.cc
  • components/network_time/network_time_tracker.cc

Estimated timestamp from git blame: 2026-03-12

Vulnerability Details

This vulnerability arises from a chain of logic errors spanning the Variations Service, Network Time Tracker, and the Certificate Verifier:

  1. Unbounded Fetch & Inflated Latency: In components/variations/service/variations_service.cc, DoFetchFromURL creates a SimpleURLLoader to download the variations seed using DownloadToStringOfUnboundedSizeUntilCrashAndDie, but fails to set a timeout duration. OnSimpleLoaderComplete calculates request latency as the entire duration from request start to full body completion (now - last_request_started_time_). An on-path attacker can trickle the response body over a prolonged duration $\Delta$, arbitrarily inflating this latency.
  2. Time Skew: VariationsService passes this inflated latency to NetworkTimeTracker::UpdateNetworkTime. The tracker estimates that the server’s time (from the HTTP Date header) corresponds to the midpoint of the latency (latency / 2). This effectively skews the tracked network time into the past by $\Delta / 2$.
  3. Discarded Uncertainty: NetworkTimeTracker correctly calculates a large uncertainty value for this measurement. However, CertVerifierServiceTimeUpdater::OnNetworkTimeChanged (in chrome/browser/net/cert_verifier_service_time_updater.cc) drops the uncertainty parameter when forwarding the time state over Mojo. CertVerifierServiceFactoryImpl::UpdateNetworkTime then initializes its internal TimeTracker with a hardcoded zero uncertainty (base::TimeDelta()).
  4. Validation Bypass: CertVerifyProcBuiltin::VerifyInternal uses this custom, zero-uncertainty network time for certificate validation. Because the time is trusted implicitly, an expired certificate that was valid $\Delta / 2$ ago will be considered currently valid. When TryBuildPath succeeds with the custom time, the verification loop breaks immediately, completely skipping any fallback checks against the true system clock.

Potential Attacker Steps

Note: These steps are based on static code analysis; an automated agent has not executed a live proof-of-concept.

  1. Intercept Seed Fetch: An on-path attacker monitors for Chrome’s Variations Seed fetch (e.g., to clients4.google.com).
  2. Trickle Body: The attacker permits the TLS-encrypted headers to reach Chrome immediately but stalls the delivery of the TLS-opaque response body over an extended duration $\Delta$ (e.g., 2 hours) using TCP window manipulation or simple packet delays.
  3. Skew Time: Once the body transfer completes, Chrome’s internal network time is skewed $\Delta/2$ (1 hour) into the past, with its uncertainty wiped out during Mojo IPC.
  4. Impersonate Target: Within the next several hours (before the next randomized background time fetch), the attacker MITMs a connection to a target origin (e.g., https://target.com).
  5. Bypass Expiration: The attacker presents a certificate for the target origin that expired less than $\Delta/2$ ago. Chrome’s CertVerifier uses the skewed past-time, finds the certificate valid, and allows the attacker to spoof the origin.

Proposed Fix

  1. Enforce Timeouts: Call SetTimeoutDuration() on the SimpleURLLoader used in VariationsService::DoFetchFromURL to prevent arbitrary latency inflation.
  2. Plumb Uncertainty: Update the CertVerifierServiceFactory::UpdateNetworkTime Mojo interface to accept the uncertainty parameter, and ensure CertVerifierServiceTimeUpdater forwards it.
  3. Enforce Limits: Update CertVerifyProcBuiltin or the internal TimeTracker to reject or deprioritize network time if the uncertainty is excessively large or if the deviation from the system clock is unreasonably high.

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.

View on issue tracker
Links in the report