Low chrome Logic Error 🔧 Commit mapped

Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in SVG
DescriptionInappropriate implementation in SVG
ComponentSVG
Bug ClassLogic Error
Tracker517759257
Fix commit64e9fccd2003 (chromium/src) +85/-4
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Files Changed

  • third_party/blink/renderer/core/css/properties/longhands/longhands_custom.cc
  • third_party/blink/web_tests/wpt_internal/svg/painting/visited-link-flood-color-alpha.html
  • third_party/blink/web_tests/wpt_internal/svg/painting/visited-link-stop-color-alpha.html
From 64e9fccd20038c43e6976cd2398612e1b8caca4a Mon Sep 17 00:00:00 2001
From: Divyansh Mangal <[email protected]>
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 <[email protected]>
Reviewed-by: Vinay Singh <[email protected]>
Commit-Queue: Divyansh Mangal <[email protected]>
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.