Chrome · CSS
CVE-2026-14098
Logic Error in CSS
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Files Changed
third_party/blink/renderer/core/style/computed_style.ccthird_party/blink/web_tests/wpt_internal/svg/painting/visited-link-context-alpha-ref.htmlthird_party/blink/web_tests/wpt_internal/svg/painting/visited-link-context-fill-alpha.htmlthird_party/blink/web_tests/wpt_internal/svg/painting/visited-link-context-stroke-alpha.html
Patch
From bb0351bff33b0883d305f03ba1d83b814290481d Mon Sep 17 00:00:00 2001
From: Divyansh Mangal <dmangal@microsoft.com>
Date: Thu, 21 May 2026 07:11:55 -0700
Subject: [PATCH] Clamp :visited alpha in VisitedDependentContextPaint
`VisitedDependentContextPaint()` returned the `:visited` color with
its own alpha channel, bypassing the history-sniffing mitigation
that requires `:visited` styles to never change element
transparency.
The standard resolver `VisitedDependentColor()` correctly constructs
the return value using visited RGB with the unvisited alpha. This CL
makes `VisitedDependentContextPaint()` apply the same alpha clamping
for both the forced-colors and normal code paths.
Bug: 513375767
Change-Id: I6014f1d11821be93f92addd5d35c6e7c40626641
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7859132
Reviewed-by: Rune Lillesveen <futhark@chromium.org>
Commit-Queue: Divyansh Mangal <dmangal@microsoft.com>
Reviewed-by: Virali Purbey <viralipurbey@microsoft.com>
Cr-Commit-Position: refs/heads/main@{#1634245}
---
diff --git a/third_party/blink/renderer/core/style/computed_style.cc b/third_party/blink/renderer/core/style/computed_style.cc
index 8929685..1e808218 100644
--- a/third_party/blink/renderer/core/style/computed_style.cc
+++ b/third_party/blink/renderer/core/style/computed_style.cc
@@ -2639,11 +2639,20 @@
if (!context_visited_paint.HasColor()) {
return unvisited_color;
}
+ blink::Color visited_color;
if (ShouldForceColor(context_visited_paint.GetColor())) {
- return GetInternalForcedVisitedCurrentColor(nullptr);
+ visited_color = GetInternalForcedVisitedCurrentColor(nullptr);
+ } else {
+ visited_color = context_visited_paint.GetColor().Resolve(
+ GetInternalVisitedCurrentColor(), UsedColorScheme(), nullptr);
}
- return context_visited_paint.GetColor().Resolve(
- GetInternalVisitedCurrentColor(), UsedColorScheme(), nullptr);
+ // Take the RGB from the visited color, but clamp alpha to the unvisited
+ // color's alpha. This prevents :visited from changing transparency, which
+ // would allow history sniffing via pixel-based side channels.
+ return Color::FromColorSpace(visited_color.GetColorSpace(),
+ visited_color.Param0(), visited_color.Param1(),
+ visited_color.Param2(),
+ unvisited_color.Alpha());
}
blink::Color ComputedStyle::ResolvedColor(const StyleColor& color,
diff --git a/third_party/blink/web_tests/wpt_internal/svg/painting/visited-link-context-alpha-ref.html b/third_party/blink/web_tests/wpt_internal/svg/painting/visited-link-context-alpha-ref.html
new file mode 100644
index 0000000..a0b91a16
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/svg/painting/visited-link-context-alpha-ref.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<style>body { margin: 0; }</style>
+<div style="width: 100px; height: 100px; background: rgb(0, 128, 0);"></div>
diff --git a/third_party/blink/web_tests/wpt_internal/svg/painting/visited-link-context-fill-alpha.html b/third_party/blink/web_tests/wpt_internal/svg/painting/visited-link-context-fill-alpha.html
new file mode 100644
index 0000000..7e9ff8e
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/svg/painting/visited-link-context-fill-alpha.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<title>:visited cannot change alpha of SVG context-fill</title>
+<link rel="help" href="https://drafts.csswg.org/selectors-4/#link">
+<link rel="help" href="https://svgwg.org/svg2-draft/painting.html#SpecifyingPaint">
+<link rel="match" href="visited-link-context-alpha-ref.html">
+<!--
+Test that :visited styles cannot change the alpha of SVG context-fill.
+The :visited privacy mitigation requires that the alpha channel of a
+resolved :visited color is clamped to the unvisited alpha.
+
+If this test passes, one should see a 100x100 green square.
+
+If the bug is present, the square would be transparent (showing the
+white background) because the visited alpha (0) leaked through context-fill.
+-->
+<style>body { margin: 0; }</style>
+<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
+ <style>
+ a .ctx { fill: rgba(0, 128, 0, 1); }
+ a:visited .ctx { fill: rgba(0, 128, 0, 0); }
+ </style>
+ <defs>
+ <symbol id="sq">
+ <rect width="100" height="100" fill="context-fill"/>
+ </symbol>
+ </defs>
+ <a href="">
+ <use class="ctx" href="#sq"/>
+ </a>
+</svg>
diff --git a/third_party/blink/web_tests/wpt_internal/svg/painting/visited-link-context-stroke-alpha.html b/third_party/blink/web_tests/wpt_internal/svg/painting/visited-link-context-stroke-alpha.html
new file mode 100644
index 0000000..6a20516
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/svg/painting/visited-link-context-stroke-alpha.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<title>:visited cannot change alpha of SVG context-stroke</title>
+<link rel="help" href="https://drafts.csswg.org/selectors-4/#link">
+<link rel="help" href="https://svgwg.org/svg2-draft/painting.html#SpecifyingPaint">
+<link rel="match" href="visited-link-context-alpha-ref.html">
+<!--
+Test that :visited styles cannot change the alpha of SVG context-stroke.
+The :visited privacy mitigation requires that the alpha channel of a
+resolved :visited color is clamped to the unvisited alpha.
+
+If this test passes, one should see a 100x100 green square.
+
+If the bug is present, the square would be transparent (showing the
+white background) because the visited alpha (0) leaked through context-stroke.
+-->
+<style>body { margin: 0; }</style>
+<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
+ <style>
+ a .ctx { stroke: rgba(0, 128, 0, 1); }
+ a:visited .ctx { stroke: rgba(0, 128, 0, 0); }
+ </style>
+ <defs>
+ <symbol id="sq">
+ <line x1="50" y1="0" x2="50" y2="100"
+ fill="none" stroke="context-stroke" stroke-width="100"/>
+ </symbol>
+ </defs>
+ <a href="">
+ <use class="ctx" href="#sq"/>
+ </a>
+</svg>
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/third_party/blink/web_tests/wpt_internal/svg/painting/visited-link-context-alpha-ref.html b/third_party/blink/web_tests/wpt_internal/svg/painting/visited-link-context-alpha-ref.html
new file mode 100644
index 0000000..a0b91a16
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/svg/painting/visited-link-context-alpha-ref.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<style>body { margin: 0; }</style>
+<div style="width: 100px; height: 100px; background: rgb(0, 128, 0);"></div>
diff --git a/third_party/blink/web_tests/wpt_internal/svg/painting/visited-link-context-fill-alpha.html b/third_party/blink/web_tests/wpt_internal/svg/painting/visited-link-context-fill-alpha.html
new file mode 100644
index 0000000..7e9ff8e
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/svg/painting/visited-link-context-fill-alpha.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<title>:visited cannot change alpha of SVG context-fill</title>
+<link rel="help" href="https://drafts.csswg.org/selectors-4/#link">
+<link rel="help" href="https://svgwg.org/svg2-draft/painting.html#SpecifyingPaint">
+<link rel="match" href="visited-link-context-alpha-ref.html">
+<!--
+Test that :visited styles cannot change the alpha of SVG context-fill.
+The :visited privacy mitigation requires that the alpha channel of a
+resolved :visited color is clamped to the unvisited alpha.
+
+If this test passes, one should see a 100x100 green square.
+
+If the bug is present, the square would be transparent (showing the
+white background) because the visited alpha (0) leaked through context-fill.
+-->
+<style>body { margin: 0; }</style>
+<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
+ <style>
+ a .ctx { fill: rgba(0, 128, 0, 1); }
+ a:visited .ctx { fill: rgba(0, 128, 0, 0); }
+ </style>
+ <defs>
+ <symbol id="sq">
+ <rect width="100" height="100" fill="context-fill"/>
+ </symbol>
+ </defs>
+ <a href="">
+ <use class="ctx" href="#sq"/>
+ </a>
+</svg>
diff --git a/third_party/blink/web_tests/wpt_internal/svg/painting/visited-link-context-stroke-alpha.html b/third_party/blink/web_tests/wpt_internal/svg/painting/visited-link-context-stroke-alpha.html
new file mode 100644
index 0000000..6a20516
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/svg/painting/visited-link-context-stroke-alpha.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<title>:visited cannot change alpha of SVG context-stroke</title>
+<link rel="help" href="https://drafts.csswg.org/selectors-4/#link">
+<link rel="help" href="https://svgwg.org/svg2-draft/painting.html#SpecifyingPaint">
+<link rel="match" href="visited-link-context-alpha-ref.html">
+<!--
+Test that :visited styles cannot change the alpha of SVG context-stroke.
+The :visited privacy mitigation requires that the alpha channel of a
+resolved :visited color is clamped to the unvisited alpha.
+
+If this test passes, one should see a 100x100 green square.
+
+If the bug is present, the square would be transparent (showing the
+white background) because the visited alpha (0) leaked through context-stroke.
+-->
+<style>body { margin: 0; }</style>
+<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
+ <style>
+ a .ctx { stroke: rgba(0, 128, 0, 1); }
+ a:visited .ctx { stroke: rgba(0, 128, 0, 0); }
+ </style>
+ <defs>
+ <symbol id="sq">
+ <line x1="50" y1="0" x2="50" y2="100"
+ fill="none" stroke="context-stroke" stroke-width="100"/>
+ </symbol>
+ </defs>
+ <a href="">
+ <use class="ctx" href="#sq"/>
+ </a>
+</svg>
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page