Medium CVSS 8.1 webkit Cross Origin 🔧 Commit mapped

Overview

Medium
Severity
8.1
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionWebsites may know if the user has visited a given link
ComponentWebCore Page
Bug ClassCross Origin
Tracker316827
Fix commit52cacff4c9e7 (WebKit/WebKit) +11/-3
CWECWE-203
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N
CISA KEVNot listed
CreditedKwak Kiyong, Song Nuri
Disclosed2026-07-27

Background

Largest Contentful Paint (LCP)
A performance API reporting when the largest content element rendered, exposed to script via PerformanceObserver.
Timing side channel
Using precise time measurements to infer secret state (cache hits, visited links) that is otherwise not directly readable.
Resolution coarsening
Rounding timestamps to a coarse quantum (here 4ms) to deny attackers fine-grained timing oracles.

Root Cause Analysis

This fixes a high-resolution timing side channel in the Largest Contentful Paint (LCP) performance entry that could be used to infer cross-origin/visited state. LargestContentfulPaintData::potentiallyAddLargestContentfulPaintEntry set the entry’s renderTime directly to the raw paintTimestamp for every entry (pendingEntry->setRenderTime(paintTimestamp)), exposing an unrounded paint time to script via the PerformanceObserver LCP API. The LCP specification requires renderTime for image entries to be coarsened to 4ms to avoid exactly this kind of timing oracle. Because the value was not coarsened, a page could measure fine-grained render timing and use it as a side channel — the advisory states the concrete consequence is that ‘websites may know if the user has visited a given link,’ i.e. differences in paint/render timing (for example driven by :visited styling or cached-resource paint timing) leak browsing history.

The fix moves renderTime assignment into the image branch, where it coarsens the value with a reduceResolution helper to a 4ms resolution (renderTimeSecondsResolution = 4_ms, floor(value/resolution)*resolution) per the spec, and keeps the raw paintTimestamp only for the non-image branch.

The restored invariant is that image LCP render times are quantized to 4ms so they cannot serve as a fine-grained timing oracle. This is an information-disclosure fix, not memory corruption.

Key insight
LCP renderTime was exposed at full resolution instead of the spec-mandated 4ms coarsening, turning paint timing into a side channel for visited-link/cache inference.

Attack Path

  1. Observe LCP entries Register a PerformanceObserver for ’largest-contentful-paint’ to receive renderTime for rendered elements.
  2. Induce a timing-dependent paint Lay out content whose paint timing depends on a secret — e.g. :visited link styling or a cross-origin resource’s cache state.
  3. Read fine-grained renderTime Use the unrounded renderTime to distinguish the two timing outcomes.
  4. Infer the secret Deduce whether the link was visited / resource was cached, leaking browsing history.

Impact Assessment

An information-disclosure / privacy issue confined to the WebContent process: a high-resolution render-time oracle that can leak whether a user has visited a link or otherwise reveal cross-origin/cache state. There is no memory corruption; the risk is browsing-history and state inference, which the 4ms coarsening blunts in line with the spec.

Changed Functions

FunctionChangeNotes
LargestContentfulPaintData::potentiallyAddLargestContentfulPaintEntry
Source/WebCore/page/LargestContentfulPaintData.cpp
modified Coarsens image entries' renderTime to 4ms via a reduceResolution(floor(value/resolution)*resolution) helper per the LCP spec, and only uses the raw paintTimestamp for non-image entries.

Files Changed

  • Source/WebCore/page/LargestContentfulPaintData.cpp

Audit Directions

  • Other performance timestamps
    Audit Performance* entry types (paint timing, resource timing, element timing) for renderTime/startTime values exposed without reduced-resolution coarsening.
  • reducedResolution usage
    Grep for relativeTimeFromTimeOriginInReducedResolution vs raw timestamps to find timing values that skip coarsening before reaching script.
diff --git a/Source/WebCore/page/LargestContentfulPaintData.cpp b/Source/WebCore/page/LargestContentfulPaintData.cpp
index 772b835081bc..45d59aa9db63 100644
--- a/Source/WebCore/page/LargestContentfulPaintData.cpp
+++ b/Source/WebCore/page/LargestContentfulPaintData.cpp
@@ -198,13 +198,21 @@ void LargestContentfulPaintData::potentiallyAddLargestContentfulPaintEntry(Eleme
         pendingEntry->setURLString(image->url().string());
         auto loadTimestamp = protect(window->performance())->relativeTimeFromTimeOriginInReducedResolution(loadTime);
         pendingEntry->setLoadTime(loadTimestamp);
-    }
+
+        // FIXME: Adopt ReducedResolutionSeconds: webkit.org/b/316824.
+        auto reduceResolution = [](Seconds value, Seconds resolution) {
+            return Seconds(std::floor(value.value() / resolution.value()) * resolution.value());
+        };
+
+        // https://w3c.github.io/largest-contentful-paint/#sec-report-largest-contentful-paint coarsens renderTime to 4ms for images.
+        static constexpr auto renderTimeSecondsResolution = 4_ms;
+        pendingEntry->setRenderTime(reduceResolution(Seconds::fromMilliseconds(paintTimestamp), renderTimeSecondsResolution).milliseconds());
+    } else
+        pendingEntry->setRenderTime(paintTimestamp);
 
     if (element.hasID())
         pendingEntry->setID(element.getIdAttribute().string());
 
-    pendingEntry->setRenderTime(paintTimestamp);
-
     LOG_WITH_STREAM(LargestContentfulPaint, stream << " making new entry for " << element << " image " << (image ? image->url().string() : emptyString()) << " id " << pendingEntry->id() <<
         ": entry size " << pendingEntry->size() << ", loadTime " << pendingEntry->loadTime() << ", renderTime " << pendingEntry->renderTime());
 
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker.