CVE-2026-11180
Overview
Files Changed
third_party/blink/renderer/core/svg/svg_animated_color.ccthird_party/blink/web_tests/external/wpt/svg/animations/animate-currentcolor-in-visited-link.html
Patch
From 3bcf4e658cdcc6e18287c949a92b97cdab9aad70 Mon Sep 17 00:00:00 2001
From: Fredrik Söderquist <fs@opera.com>
Date: Wed, 22 Apr 2026 15:00:55 -0700
Subject: [PATCH] Always resolve SMIL animated 'currentcolor' using the unvisited 'color'
Fixed: 502631225
Change-Id: I342a5d303a6366b0b9f8763b34d81b7335e5632f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7764330
Commit-Queue: Philip Rogers <pdr@chromium.org>
Auto-Submit: Fredrik Söderquist <fs@opera.com>
Reviewed-by: Philip Rogers <pdr@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1619117}
---
diff --git a/third_party/blink/renderer/core/svg/svg_animated_color.cc b/third_party/blink/renderer/core/svg/svg_animated_color.cc
index 1a2c70a..28e9a99 100644
--- a/third_party/blink/renderer/core/svg/svg_animated_color.cc
+++ b/third_party/blink/renderer/core/svg/svg_animated_color.cc
@@ -60,8 +60,11 @@
}
Color FallbackColorForCurrentColor(const SVGElement& target_element) {
+ // As a workaround, always use the unvisited 'color' when resolving a
+ // potential 'currentcolor' value to prevent leaking :visited state.
if (const ComputedStyle* target_style = target_element.GetComputedStyle()) {
- return target_style->VisitedDependentColor(GetCSSPropertyColor());
+ return GetCSSPropertyColor().ColorIncludingFallback(
+ /*visited_link=*/false, *target_style, /*is_current_color=*/nullptr);
}
return Color::kTransparent;
}
diff --git a/third_party/blink/web_tests/external/wpt/svg/animations/animate-currentcolor-in-visited-link.html b/third_party/blink/web_tests/external/wpt/svg/animations/animate-currentcolor-in-visited-link.html
new file mode 100644
index 0000000..be03258
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/svg/animations/animate-currentcolor-in-visited-link.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<title>SMIL animating 'currentcolor' on target within visited link</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="help" href="https://drafts.csswg.org/selectors/#visited-privacy">
+<style>
+ a:link { color: blue; }
+ a:visited { color: lightblue; }
+</style>
+<a href="">
+ <svg>
+ <rect id="r" width="100" height="100">
+ <set attributeName="fill" to="currentcolor"/>
+ </rect>
+ </svg>
+</a>
+<script>
+ async_test(t => {
+ const rect = document.getElementById('r');
+ const set = rect.firstElementChild;
+ set.onbegin = t.step_func_done(() => {
+ const style = getComputedStyle(rect);
+ assert_equals(style.fill, style.color, `currentcolor animation does not leak :visited`);
+ });
+ });
+</script>
Regression Test / PoC
diff --git a/third_party/blink/web_tests/external/wpt/svg/animations/animate-currentcolor-in-visited-link.html b/third_party/blink/web_tests/external/wpt/svg/animations/animate-currentcolor-in-visited-link.html
new file mode 100644
index 0000000..be03258
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/svg/animations/animate-currentcolor-in-visited-link.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<title>SMIL animating 'currentcolor' on target within visited link</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="help" href="https://drafts.csswg.org/selectors/#visited-privacy">
+<style>
+ a:link { color: blue; }
+ a:visited { color: lightblue; }
+</style>
+<a href="">
+ <svg>
+ <rect id="r" width="100" height="100">
+ <set attributeName="fill" to="currentcolor"/>
+ </rect>
+ </svg>
+</a>
+<script>
+ async_test(t => {
+ const rect = document.getElementById('r');
+ const set = rect.firstElementChild;
+ set.onbegin = t.step_func_done(() => {
+ const style = getComputedStyle(rect);
+ assert_equals(style.fill, style.color, `currentcolor animation does not leak :visited`);
+ });
+ });
+</script>
Original Bug Report
Cross-origin history leak via SVG SMIL currentColor animation
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team.
Overview: A logic error in SVG SMIL animations eagerly resolves the currentColor keyword using visited-dependent styles. This resolved absolute color is then applied to the CSS cascade, overriding the element’s unvisited style slot with the visited color. This allows JavaScript to deterministically read the user’s cross-origin browsing history via getComputedStyle().
Affected files:
third_party/blink/renderer/core/svg/svg_animated_color.ccthird_party/blink/renderer/core/svg/svg_animate_element.ccthird_party/blink/renderer/core/css/resolver/style_resolver.ccthird_party/blink/renderer/core/css/element_rule_collector.ccthird_party/blink/renderer/core/css/properties/longhands/longhands_custom.cc
Estimated timestamp from git blame: 2025-10-21
Description
A potential vulnerability exists in the handling of SVG SMIL animations for color properties, leading to a deterministic cross-origin history leak. This allows an attacker to bypass the dual-style mitigation for :visited links and read whether a specific URL is in the user’s history.
When a SMIL animation (e.g., <animate>) targets a color attribute using currentColor inside a visited link, the following sequence occurs:
- Eager Resolution: During animation processing,
SVGColorProperty::CalculateAnimatedValuecalculates the frame’s color. To handlecurrentColor, it callsFallbackColorForCurrentColor, which queries the element’sComputedStyleviaVisitedDependentColor. Because the element is inside a visited link, this returns the absolute visited color. - Semantic Loss: The animation engine converts this absolute color back to a
StyleColor, permanently losing thecurrentColorkeyword semantic. The animation result is then serialized to an absolute RGB string (e.g.,"rgb(255, 255, 255)"). - Cascade Laundering:
SVGAnimateElement::ApplyResultsToTargetapplies this absolute string as a SMIL style property. During style resolution,StyleResolveradds these properties to the cascade. Because the element is inside a link, the match type is set toCSSSelector::kMatchAll. - Slot Override: In
StyleCascade::CollectFromMatchResult,kMatchAllcauses the property to be expanded into both its unvisited and visited (-internal-visited-*) slots. Consequently, the unvisited style slot is overwritten with the absolute visited color. - Information Leak: When JavaScript calls
getComputedStyle().fill, which is designed to only return the unvisited style, it returns the absolute visited color, successfully leaking the history state.
Impact
This provides a deterministic, zero-interaction readback of a user’s cross-origin browsing history. While PartitionVisitedLinkDatabase mitigates the impact on standard Chrome (partitioning history by top-level site), the vulnerability exposes unpartitioned cross-origin history on Android WebView, where link partitioning is explicitly disabled.
Potential Reproduction Steps
(Note: These are suggested steps based on code analysis; our tooling agent cannot execute code to verify.)
- Serve a page containing the following structure:
<style> a:link { color: #000; } a:visited { color: #fff; } </style> <a href='https://target.example/'> <svg> <rect id='target' width='10' height='10'> <animate attributeName='fill' from='currentColor' to='currentColor' begin='0s' dur='1s' fill='freeze'/> </rect> </svg> </a> - Wait for the animation to apply (e.g., using
requestAnimationFrametwice). - Read the computed style of the rectangle:
const color = getComputedStyle(document.getElementById('target')).fill; - If the URL
https://target.example/has been visited,colorwill be'rgb(255, 255, 255)'. Otherwise, it will be'rgb(0, 0, 0)'.
Suggested Fix
The root cause is the eager resolution of currentColor during the SMIL animation phase, which drops the keyword semantic and locks in the visited color.
To fix this, SMIL animations should ideally preserve the currentColor keyword (or UnresolvedColorFunction semantics) throughout the animation calculation and serialization phases, delaying resolution until the final ComputedStyle is built. Alternatively, if eager resolution is required for interpolation, the engine must track whether the resulting color was influenced by a visited state and ensure that the serialized SMIL property is applied exclusively to the visited style slot, rather than broadcasting it to the unvisited slot via kMatchAll.
Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.