Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper input validation in Variations
DescriptionImproper input validation in Variations
ComponentVariations
Bug ClassLogic Error
Tracker502082953
Fix commit823df872a358 (chromium/src) +27/-13
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Files Changed

  • chrome/browser/net/cert_verifier_service_time_updater.cc
  • services/cert_verifier/cert_verifier_service_factory.cc
  • services/cert_verifier/cert_verifier_service_factory.h
  • services/cert_verifier/cert_verifier_service_factory_unittest.cc
  • services/cert_verifier/public/mojom/cert_verifier_service_factory.mojom
From 823df872a35856b0d49804c3bd097810bebea534 Mon Sep 17 00:00:00 2001
From: Roger McFarlane <rogerm@chromium.org>
Date: Thu, 23 Apr 2026 12:10:17 -0700
Subject: [PATCH] Plumb network time uncertainty to CertVerifierService.

This CL updates the CertVerifierServiceFactory::UpdateNetworkTime Mojo
interface to accept the NetworkTimeTracker's uncertainty parameter,
and ensures that the CertVerifierServiceTimeUpdater forwards it.

This allows the CertVerifierService to consider network time uncertainty
and skew when deciding on the validity of a certificate.

Bug: 502082953
Change-Id: I62df04169de2ee8eb90e14f4017d786e1f5a5e54
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7765821
Reviewed-by: Matt Mueller <mattm@chromium.org>
Reviewed-by: Joe Mason <joenotcharles@google.com>
Reviewed-by: Alexei Svitkine <asvitkine@chromium.org>
Commit-Queue: Roger McFarlane <rogerm@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1619674}
---

diff --git a/chrome/browser/net/cert_verifier_service_time_updater.cc b/chrome/browser/net/cert_verifier_service_time_updater.cc
index 979cd79..e0535185 100644
--- a/chrome/browser/net/cert_verifier_service_time_updater.cc
+++ b/chrome/browser/net/cert_verifier_service_time_updater.cc
@@ -17,7 +17,8 @@
   network_time::TimeTracker::TimeTrackerState state;
   if (tracker->GetTrackerState(&state)) {
     content::GetCertVerifierServiceFactory()->UpdateNetworkTime(
-        state.system_time, state.system_ticks, state.known_time);
+        state.system_time, state.system_ticks, state.known_time,
+        state.uncertainty);
   }
 }
 
@@ -26,5 +27,6 @@
 void CertVerifierServiceTimeUpdater::OnNetworkTimeChanged(
     network_time::TimeTracker::TimeTrackerState state) {
   content::GetCertVerifierServiceFactory()->UpdateNetworkTime(
-      state.system_time, state.system_ticks, state.known_time);
+      state.system_time, state.system_ticks, state.known_time,
+      state.uncertainty);
 }
diff --git a/services/cert_verifier/cert_verifier_service_factory.cc b/services/cert_verifier/cert_verifier_service_factory.cc
index 6efcda37b..30d0b20 100644
--- a/services/cert_verifier/cert_verifier_service_factory.cc
+++ b/services/cert_verifier/cert_verifier_service_factory.cc
@@ -510,9 +510,11 @@
 void CertVerifierServiceFactoryImpl::UpdateNetworkTime(
     base::Time system_time,
     base::TimeTicks system_ticks,
-    base::Time current_time) {
+    base::Time current_time,
+    base::TimeDelta uncertainty) {
+  // TODO: crbug.com/502082953 - Consider what to do if the uncertainty is high.
   proc_params_.time_tracker.emplace(system_time, system_ticks, current_time,
-                                    base::TimeDelta());
+                                    uncertainty);
   UpdateVerifierServices();
 }
 
diff --git a/services/cert_verifier/cert_verifier_service_factory.h b/services/cert_verifier/cert_verifier_service_factory.h
index f6239094..ba3a9f5 100644
--- a/services/cert_verifier/cert_verifier_service_factory.h
+++ b/services/cert_verifier/cert_verifier_service_factory.h
@@ -89,7 +89,8 @@
 #endif
   void UpdateNetworkTime(base::Time system_time,
                          base::TimeTicks system_ticks,
-                         base::Time current_time) override;
+                         base::Time current_time,
+                         base::TimeDelta uncertainty) override;
 
   // Remove a CertVerifyService from needing updates to the Chrome Root Store.
   void RemoveService(internal::CertVerifierServiceImpl* service_impl);
diff --git a/services/cert_verifier/cert_verifier_service_factory_unittest.cc b/services/cert_verifier/cert_verifier_service_factory_unittest.cc
index d35fe3f..61bfb2c 100644
--- a/services/cert_verifier/cert_verifier_service_factory_unittest.cc
+++ b/services/cert_verifier/cert_verifier_service_factory_unittest.cc
@@ -68,6 +68,8 @@
 namespace cert_verifier {
 namespace {
 
+constexpr base::TimeDelta kNetworkTimeUncertainty = base::Seconds(31);
+
 struct DummyCVServiceRequest : public mojom::CertVerifierRequest {
   explicit DummyCVServiceRequest(base::RepeatingClosure on_finish)
       : on_finish_(std::move(on_finish)) {}
@@ -1519,7 +1521,8 @@
   // Update the time tracker so the current time is within the certificate
   // validity range.
   cv_service_factory_impl.UpdateNetworkTime(now, ticks_now,
-                                            now - base::Days(2));
+                                            now - base::Days(2),
+                                            kNetworkTimeUncertainty);
 
   mojo::Remote<mojom::CertVerifierService> cv_service_remote;
   DummyCVServiceClient cv_service_client;
@@ -1587,7 +1590,8 @@
   // Update the time tracker so the current time is within the certificate
   // validity range.
   cv_service_factory_impl.UpdateNetworkTime(now, ticks_now,
-                                            now - base::Days(2));
+                                            now - base::Days(2),
+                                            kNetworkTimeUncertainty);
 
   // Update should have been notified.
   EXPECT_NO_FATAL_FAILURE(cv_service_client.WaitForCertVerifierChange(1u));
diff --git a/services/cert_verifier/public/mojom/cert_verifier_service_factory.mojom b/services/cert_verifier/public/mojom/cert_verifier_service_factory.mojom
index 8afc587..6453cbdb 100644
--- a/services/cert_verifier/public/mojom/cert_verifier_service_factory.mojom
+++ b/services/cert_verifier/public/mojom/cert_verifier_service_factory.mojom
@@ -132,12 +132,17 @@
   [EnableIf=is_platform_root_store_info_supported]
   GetPlatformRootStoreInfo() => (PlatformRootStoreInfo root_store_info);
 
-  // Updates the time used for certificate verification. After this is called,
-  // the system time will be recorded along with a known time passed in as
-  // `current_time`. Future verification attempts will estimate the real time
-  // based on how much the system clock has advanced since the last update.
-  // This only has an effect when the built-in verifier is being used.
+  // Synchronizes the clock used for certificate verification. After this is
+  // called, the local machine's time (recorded as both wall clock system time
+  // and system ticks) will be correlated with a known current_time and an
+  // estimate of uncertainty. Future certificate verification attempts will
+  // estimate the real time based on how much the system clock has advanced
+  // since the last update. This only has an effect when the built-in verifier
+  // is being used. The CertVerifierService implementation may choose to ignore
+  // or adjust the synchronziation request if the uncertainty is too high (impl
+  // defined)
   UpdateNetworkTime(mojo_base.mojom.Time system_time,
                     mojo_base.mojom.TimeTicks system_ticks,
-                    mojo_base.mojom.Time current_time);
+                    mojo_base.mojom.Time current_time,
+                    mojo_base.mojom.TimeDelta uncertainty);
 };
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 d35fe3f..61bfb2c 100644
--- a/services/cert_verifier/cert_verifier_service_factory_unittest.cc
+++ b/services/cert_verifier/cert_verifier_service_factory_unittest.cc
@@ -68,6 +68,8 @@
 namespace cert_verifier {
 namespace {
 
+constexpr base::TimeDelta kNetworkTimeUncertainty = base::Seconds(31);
+
 struct DummyCVServiceRequest : public mojom::CertVerifierRequest {
   explicit DummyCVServiceRequest(base::RepeatingClosure on_finish)
       : on_finish_(std::move(on_finish)) {}
@@ -1519,7 +1521,8 @@
   // Update the time tracker so the current time is within the certificate
   // validity range.
   cv_service_factory_impl.UpdateNetworkTime(now, ticks_now,
-                                            now - base::Days(2));
+                                            now - base::Days(2),
+                                            kNetworkTimeUncertainty);
 
   mojo::Remote<mojom::CertVerifierService> cv_service_remote;
   DummyCVServiceClient cv_service_client;
@@ -1587,7 +1590,8 @@
   // Update the time tracker so the current time is within the certificate
   // validity range.
   cv_service_factory_impl.UpdateNetworkTime(now, ticks_now,
-                                            now - base::Days(2));
+                                            now - base::Days(2),
+                                            kNetworkTimeUncertainty);
 
   // Update should have been notified.
   EXPECT_NO_FATAL_FAILURE(cv_service_client.WaitForCertVerifierChange(1u));
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