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
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/Source/WebCore/rendering/InlineBoxPainter.cpp b/Source/WebCore/rendering/InlineBoxPainter.cpp
index fa1b4c6d4221..08f7eabd1549 100644
--- a/Source/WebCore/rendering/InlineBoxPainter.cpp
+++ b/Source/WebCore/rendering/InlineBoxPainter.cpp
@@ -238,7 +238,7 @@ void InlineBoxPainter::paintDecorations()
if (!BackgroundPainter::boxShadowShouldBeAppliedToBackground(renderer(), adjustedPaintoffset, BackgroundBleedNone, m_inlineBox))
paintBoxShadow(ShadowStyle::Normal, paintRect);
- auto color = style.visitedDependentColor(CSSPropertyBackgroundColor);
+ auto color = style.visitedDependentColor(CSSPropertyBackgroundColor, m_paintInfo.paintBehavior);
auto compositeOp = renderer().document().compositeOperatorForBackgroundColor(color, renderer());
color = style.colorByApplyingColorFilter(color);
diff --git a/LayoutTests/css3/filters/filter-visited-links-expected.html b/LayoutTests/css3/filters/filter-visited-links-expected.html
index d437c49f7f21..a9b554ce670b 100644
--- a/LayoutTests/css3/filters/filter-visited-links-expected.html
+++ b/LayoutTests/css3/filters/filter-visited-links-expected.html
@@ -27,9 +27,11 @@
a {
color: blue;
text-decoration: none;
+ background-color: yellow;
}
a:visited {
color: purple;
+ background-color: orange;
}
</style>
<body>
diff --git a/LayoutTests/css3/filters/filter-visited-links.html b/LayoutTests/css3/filters/filter-visited-links.html
index 2c578423fdae..45b19c84ca95 100644
--- a/LayoutTests/css3/filters/filter-visited-links.html
+++ b/LayoutTests/css3/filters/filter-visited-links.html
@@ -27,9 +27,11 @@
a {
color: blue;
text-decoration: none;
+ background-color: yellow;
}
a:visited {
color: purple;
+ background-color: orange;
}
</style>
<body>
Regression Test / PoC
diff --git a/LayoutTests/css3/filters/filter-visited-links-expected.html b/LayoutTests/css3/filters/filter-visited-links-expected.html
index d437c49f7f21..a9b554ce670b 100644
--- a/LayoutTests/css3/filters/filter-visited-links-expected.html
+++ b/LayoutTests/css3/filters/filter-visited-links-expected.html
@@ -27,9 +27,11 @@
a {
color: blue;
text-decoration: none;
+ background-color: yellow;
}
a:visited {
color: purple;
+ background-color: orange;
}
</style>
<body>
diff --git a/LayoutTests/css3/filters/filter-visited-links.html b/LayoutTests/css3/filters/filter-visited-links.html
index 2c578423fdae..45b19c84ca95 100644
--- a/LayoutTests/css3/filters/filter-visited-links.html
+++ b/LayoutTests/css3/filters/filter-visited-links.html
@@ -27,9 +27,11 @@
a {
color: blue;
text-decoration: none;
+ background-color: yellow;
}
a:visited {
color: purple;
+ background-color: orange;
}
</style>
<body>