CVE-2024-27838
Overview
Background
- :visited privacy protections
- Browsers allow limited :visited styling but restrict readback so pages cannot detect which links a user has visited.
- visitedDependentColor + paintBehavior
- A style lookup that returns the visited or unvisited color; the paintBehavior tells it whether visited styles must be ignored for the current paint.
- SVG/CSS filter side channel
- A filter processes rendered pixels; if visited-dependent color enters the filter, the observable output can leak visited state.
Root Cause Analysis
This fixes a browsing-history disclosure (visited-link detection) via SVG/CSS filters applied to hyperlinks. WebKit lets :visited links be styled but tightly restricts what a page can observe about visited state to prevent history sniffing; painting code must ignore visited-dependent styles in contexts where the result could be read back. Applying an SVG filter to a hyperlink, however, could leak the visited state through the link’s BACKGROUND color: InlineBoxPainter::paintDecorations obtained the background color with style.visitedDependentColor(CSSPropertyBackgroundColor) WITHOUT passing the current paint behavior, so it used the visited-dependent background even when painting into a filter pipeline whose output the page can observe (per the averaging/filter attack described in arXiv:2305.12784). A page could thus set different a:visited background colors, apply an SVG filter, and infer from the filtered output whether a given link had been visited.
The fix threads the paint behavior into the lookup: style.visitedDependentColor(CSSPropertyBackgroundColor, m_paintInfo.paintBehavior), so when the paint behavior indicates visited styles must be ignored (e.g. while producing filter input), the unvisited background color is used instead of the visited-dependent one. This is a follow-up to an earlier fix (266683@main) that handled the text color; this commit closes the same leak for background-color.
The restored invariant is that visited-dependent background color is not used when painting in a context that could expose it, so filters cannot recover visited state. The regression test gives a:link and a:visited different background colors and checks the filtered rendering does not reveal visited state.
Attack Path
- Style visited backgrounds Define a:link and a:visited with different background-color values for target links.
- Apply an SVG filter Apply an SVG/CSS filter to the hyperlink so the visited-dependent background is drawn into the filter pipeline.
- Observe the filtered output Read back the filtered rendering (per the averaging attack) to distinguish the visited vs unvisited background.
- Infer history Deduce whether each link was visited, leaking the user’s browsing history.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
InlineBoxPainter::paintDecorationsSource/WebCore/rendering/InlineBoxPainter.cpp |
modified | Passes m_paintInfo.paintBehavior to style.visitedDependentColor(CSSPropertyBackgroundColor, ...) so visited-dependent background is ignored when painting in a context (e.g. filter input) that could expose visited state. |
Files Changed
LayoutTests/css3/filters/filter-visited-links-expected.htmlLayoutTests/css3/filters/filter-visited-links.htmlSource/WebCore/rendering/InlineBoxPainter.cpp
Audit Directions
- visitedDependentColor call sitesGrep for style.visitedDependentColor(…) that omit a paintBehavior argument in painting code (backgrounds, borders, shadows, decorations) reachable inside filters/readback.
- Visited-state readback surfacesReview filter, canvas-drawImage, and paint-into-image paths for use of visited-dependent styles that could leak history.
Patch
diff --git a/LayoutTests/swipe/resources/swipe-test.js b/LayoutTests/swipe/resources/swipe-test.js
index a32ba832493c..132f6c9c6c7f 100644
--- a/LayoutTests/swipe/resources/swipe-test.js
+++ b/LayoutTests/swipe/resources/swipe-test.js
@@ -46,6 +46,8 @@ async function startSlowSwipeGesture()
if (!window.eventSender)
return;
+ log("startSlowSwipeGesture");
+
await UIHelper.ensurePresentationUpdate();
// Similar to uiController.beginBackSwipe(), but with a gap between events to allow
diff --git a/LayoutTests/swipe/swipe-back-with-active-wheel-listener-expected.txt b/LayoutTests/swipe/swipe-back-with-active-wheel-listener-expected.txt
index 8215851db5ff..fd5b55d8f959 100644
--- a/LayoutTests/swipe/swipe-back-with-active-wheel-listener-expected.txt
+++ b/LayoutTests/swipe/swipe-back-with-active-wheel-listener-expected.txt
@@ -1,4 +1,5 @@
Swipe target
+startSlowSwipeGesture
didBeginSwipe
completeSwipeGesture
willEndSwipe
diff --git a/LayoutTests/swipe/swipe-back-with-passive-wheel-listener-expected.txt b/LayoutTests/swipe/swipe-back-with-passive-wheel-listener-expected.txt
index 6bd807db86af..fd5b55d8f959 100644
--- a/LayoutTests/swipe/swipe-back-with-passive-wheel-listener-expected.txt
+++ b/LayoutTests/swipe/swipe-back-with-passive-wheel-listener-expected.txt
@@ -1,5 +1,5 @@
Swipe target
-startSwipeGesture
+startSlowSwipeGesture
didBeginSwipe
completeSwipeGesture
willEndSwipe
diff --git a/LayoutTests/swipe/wheel-prevent-default-prevents-swipe-back-expected.txt b/LayoutTests/swipe/wheel-prevent-default-prevents-swipe-back-expected.txt
index e7a0ac32652b..975b6f62289c 100644
--- a/LayoutTests/swipe/wheel-prevent-default-prevents-swipe-back-expected.txt
+++ b/LayoutTests/swipe/wheel-prevent-default-prevents-swipe-back-expected.txt
@@ -1,3 +1,4 @@
Swipe target
+startSlowSwipeGesture
completeSwipeGesture