Chrome · SVG
CVE-2026-17963
Logic Error in SVG
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Files Changed
third_party/blink/renderer/core/css/properties/longhands/longhands_custom.ccthird_party/blink/web_tests/wpt_internal/svg/painting/visited-link-flood-color-alpha.htmlthird_party/blink/web_tests/wpt_internal/svg/painting/visited-link-stop-color-alpha.html
Patch
From 64e9fccd20038c43e6976cd2398612e1b8caca4a Mon Sep 17 00:00:00 2001
From: Divyansh Mangal <dmangal@microsoft.com>
Date: Mon, 08 Jun 2026 23:17:57 -0700
Subject: [PATCH] Fix :visited alpha clamp bypass in SVG stop-color, flood-color, lighting-color, and -webkit-tap-highlight-color
`StopColor::ColorIncludingFallback`,
`FloodColor::ColorIncludingFallback`,
`LightingColor::ColorIncludingFallback`, and
`WebkitTapHighlightColor::ColorIncludingFallback` ignored their
`visited_link` parameter and delegated to
`ComputedStyle::ResolvedColor()`, which dynamically queries
`InsideLink()` to select the current color basis.
Inside a visited link, both the `visited_link=false` and
`visited_link=true` calls in `VisitedDependentColor()` resolved to
the visited color, making the unvisited-alpha clamp a no-op. This
allowed `:visited` styles to change the rendered alpha of SVG
gradient stops and flood fills, leaking one bit of browsing history
into visible paint.
In this CL we fix this by resolving `currentColor` explicitly based
on the `visited_link` parameter in each override, bypassing
`ResolvedColor()`.
Bug: 517759257
Change-Id: I9fed29e8d063d4117ed459ec8f60fb80b40de2ed
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7893575
Reviewed-by: Stefan Zager <szager@chromium.org>
Reviewed-by: Vinay Singh <vinaysingh@microsoft.com>
Commit-Queue: Divyansh Mangal <dmangal@microsoft.com>
Cr-Commit-Position: refs/heads/main@{#1643718}
---
diff --git a/third_party/blink/renderer/core/css/properties/longhands/longhands_custom.cc b/third_party/blink/renderer/core/css/properties/longhands/longhands_custom.cc
index 62ffc67..c99dabe 100644
--- a/third_party/blink/renderer/core/css/properties/longhands/longhands_custom.cc
+++ b/third_party/blink/renderer/core/css/properties/longhands/longhands_custom.cc
@@ -4431,7 +4431,11 @@
if (style.ShouldForceColor(flood_color)) {
return style.GetInternalForcedCurrentColor(is_current_color);
}
- return style.ResolvedColor(flood_color, is_current_color);
+ blink::Color current_color = visited_link
+ ? style.GetInternalVisitedCurrentColor()
+ : style.GetCurrentColor();
+ return flood_color.Resolve(current_color, style.UsedColorScheme(),
+ is_current_color);
}
const CSSValue* FloodColor::CSSValueFromComputedStyleInternal(
@@ -6366,7 +6370,11 @@
if (style.ShouldForceColor(lighting_color)) {
return style.GetInternalForcedCurrentColor(is_current_color);
}
- return style.ResolvedColor(lighting_color, is_current_color);
+ blink::Color current_color = visited_link
+ ? style.GetInternalVisitedCurrentColor()
+ : style.GetCurrentColor();
+ return lighting_color.Resolve(current_color, style.UsedColorScheme(),
+ is_current_color);
}
const CSSValue* LightingColor::CSSValueFromComputedStyleInternal(
@@ -9427,7 +9435,11 @@
if (style.ShouldForceColor(stop_color)) {
return style.GetInternalForcedCurrentColor(is_current_color);
}
- return style.ResolvedColor(stop_color, is_current_color);
+ blink::Color current_color = visited_link
+ ? style.GetInternalVisitedCurrentColor()
+ : style.GetCurrentColor();
+ return stop_color.Resolve(current_color, style.UsedColorScheme(),
+ is_current_color);
}
const CSSValue* StopColor::CSSValueFromComputedStyleInternal(
@@ -11577,7 +11589,11 @@
? style.GetInternalForcedVisitedCurrentColor(is_current_color)
: style.GetInternalForcedCurrentColor(is_current_color);
}
- return style.ResolvedColor(style.TapHighlightColor(), is_current_color);
+ blink::Color current_color = visited_link
+ ? style.GetInternalVisitedCurrentColor()
+ : style.GetCurrentColor();
+ return highlight_color.Resolve(current_color, style.UsedColorScheme(),
+ is_current_color);
}
const CSSValue* WebkitTapHighlightColor::CSSValueFromComputedStyleInternal(
diff --git a/third_party/blink/web_tests/wpt_internal/svg/painting/visited-link-flood-color-alpha.html b/third_party/blink/web_tests/wpt_internal/svg/painting/visited-link-flood-color-alpha.html
new file mode 100644
index 0000000..2225be3
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/svg/painting/visited-link-flood-color-alpha.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<title>:visited cannot change alpha of SVG flood-color</title>
+<link rel="help" href="https://drafts.csswg.org/selectors-4/#link">
+<link rel="help" href="https://drafts.fxtf.org/filter-effects/#FloodColorProperty">
+<link rel="match" href="visited-link-context-alpha-ref.html">
+<!--
+Test that :visited styles cannot change the alpha of SVG flood-color
+when it resolves currentColor.
+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
+flood-color: currentColor.
+-->
+<style>
+ body { margin: 0; }
+ a:link { color: rgba(0, 128, 0, 1); }
+ a:visited { color: rgba(0, 128, 0, 0); }
+</style>
+<a href="">
+ <svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
+ <defs>
+ <filter id="f" x="0" y="0" width="100%" height="100%">
+ <feFlood flood-color="currentColor" result="flood"/>
+ </filter>
+ </defs>
+ <rect width="100" height="100" filter="url(#f)"/>
+ </svg>
+</a>
diff --git a/third_party/blink/web_tests/wpt_internal/svg/painting/visited-link-stop-color-alpha.html b/third_party/blink/web_tests/wpt_internal/svg/painting/visited-link-stop-color-alpha.html
new file mode 100644
index 0000000..29921bd
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/svg/painting/visited-link-stop-color-alpha.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<title>:visited cannot change alpha of SVG stop-color</title>
+<link rel="help" href="https://drafts.csswg.org/selectors-4/#link">
+<link rel="help" href="https://svgwg.org/svg2-draft/painting.html#StopColorProperty">
+<link rel="match" href="visited-link-context-alpha-ref.html">
+<!--
+Test that :visited styles cannot change the alpha of SVG stop-color
+when it resolves currentColor.
+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
+stop-color: currentColor.
+-->
+<style>
+ body { margin: 0;}
+ a:link { color: rgba(0, 128, 0, 1); }
+ a:visited { color: rgba(0, 128, 0, 0); }
+</style>
+<a href="">
+ <svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
+ <defs>
+ <linearGradient id="g">
+ <stop offset="0" stop-color="currentColor"/>
+ <stop offset="1" stop-color="currentColor"/>
+ </linearGradient>
+ </defs>
+ <rect width="100" height="100" fill="url(#g)"/>
+ </svg>
+</a>
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/third_party/blink/web_tests/wpt_internal/svg/painting/visited-link-flood-color-alpha.html b/third_party/blink/web_tests/wpt_internal/svg/painting/visited-link-flood-color-alpha.html
new file mode 100644
index 0000000..2225be3
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/svg/painting/visited-link-flood-color-alpha.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<title>:visited cannot change alpha of SVG flood-color</title>
+<link rel="help" href="https://drafts.csswg.org/selectors-4/#link">
+<link rel="help" href="https://drafts.fxtf.org/filter-effects/#FloodColorProperty">
+<link rel="match" href="visited-link-context-alpha-ref.html">
+<!--
+Test that :visited styles cannot change the alpha of SVG flood-color
+when it resolves currentColor.
+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
+flood-color: currentColor.
+-->
+<style>
+ body { margin: 0; }
+ a:link { color: rgba(0, 128, 0, 1); }
+ a:visited { color: rgba(0, 128, 0, 0); }
+</style>
+<a href="">
+ <svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
+ <defs>
+ <filter id="f" x="0" y="0" width="100%" height="100%">
+ <feFlood flood-color="currentColor" result="flood"/>
+ </filter>
+ </defs>
+ <rect width="100" height="100" filter="url(#f)"/>
+ </svg>
+</a>
diff --git a/third_party/blink/web_tests/wpt_internal/svg/painting/visited-link-stop-color-alpha.html b/third_party/blink/web_tests/wpt_internal/svg/painting/visited-link-stop-color-alpha.html
new file mode 100644
index 0000000..29921bd
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/svg/painting/visited-link-stop-color-alpha.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<title>:visited cannot change alpha of SVG stop-color</title>
+<link rel="help" href="https://drafts.csswg.org/selectors-4/#link">
+<link rel="help" href="https://svgwg.org/svg2-draft/painting.html#StopColorProperty">
+<link rel="match" href="visited-link-context-alpha-ref.html">
+<!--
+Test that :visited styles cannot change the alpha of SVG stop-color
+when it resolves currentColor.
+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
+stop-color: currentColor.
+-->
+<style>
+ body { margin: 0;}
+ a:link { color: rgba(0, 128, 0, 1); }
+ a:visited { color: rgba(0, 128, 0, 0); }
+</style>
+<a href="">
+ <svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
+ <defs>
+ <linearGradient id="g">
+ <stop offset="0" stop-color="currentColor"/>
+ <stop offset="1" stop-color="currentColor"/>
+ </linearGradient>
+ </defs>
+ <rect width="100" height="100" fill="url(#g)"/>
+ </svg>
+</a>
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