CVE-2026-11155
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/animation/css/css_animations.cc |
modified | |
CORE_EXPORTthird_party/blink/renderer/core/animation/css_default_interpolation_type.h |
modified |
Files Changed
third_party/blink/renderer/core/animation/css/css_animations.ccthird_party/blink/renderer/core/animation/css_default_interpolation_type.ccthird_party/blink/renderer/core/animation/css_default_interpolation_type.hthird_party/blink/renderer/core/css/properties/css_property.hthird_party/blink/renderer/core/css/properties/longhands/custom_property.ccthird_party/blink/web_tests/external/wpt/css/css-values/attr-security-animation.html
Patch
From 67362671e2817011a4547f9500e45380e3c66ca7 Mon Sep 17 00:00:00 2001
From: moonira <moonira@google.com>
Date: Fri, 17 Apr 2026 14:39:28 -0700
Subject: [PATCH] Propagate attr() taint flag to CSSDefaultNonInterpolableValue
Previously, if an `attr()` function was used in a custom property and
underwent a CSS transition, the tainted flag could be lost.
This patch ensures that the `is_attr_tainted` flag is properly
propagated to `CSSDefaultNonInterpolableValue` when transitioning
registered custom properties of type `<string>`.
Fixed: 501801823
Change-Id: I015539cf9fdaabd94e35aa5be5582e5dab7a2f4f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7772751
Reviewed-by: Anders Hartvoll Ruud <andruud@chromium.org>
Commit-Queue: Munira Tursunova <moonira@google.com>
Cr-Commit-Position: refs/heads/main@{#1616868}
---
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 5e78174c..9572e90 100644
--- a/third_party/blink/renderer/core/animation/css/css_animations.cc
+++ b/third_party/blink/renderer/core/animation/css/css_animations.cc
@@ -2656,6 +2656,7 @@
return;
}
+ bool is_attr_tainted = false;
if (!start || !end) {
const Document& document = state.animating_element.GetDocument();
const CSSValue* start_css_value =
@@ -2687,12 +2688,27 @@
// correctly. If that bug is fixed, then this should never happen.
return;
}
+ if (property.IsCSSCustomProperty()) {
+ CSSPropertyRef custom_ref(property.GetCSSPropertyName(), 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>(start_css_value));
+ MakeGarbageCollected<CSSDefaultNonInterpolableValue>(
+ start_css_value,
+ CSSDefaultNonInterpolableValue::AttrTainted(is_attr_tainted)));
end = InterpolationValue(
MakeGarbageCollected<InterpolableList>(0),
- MakeGarbageCollected<CSSDefaultNonInterpolableValue>(end_css_value));
+ MakeGarbageCollected<CSSDefaultNonInterpolableValue>(
+ end_css_value,
+ CSSDefaultNonInterpolableValue::AttrTainted(is_attr_tainted)));
}
// If the interpolated transform lists contain any singular matrices, a
diff --git a/third_party/blink/renderer/core/animation/css_default_interpolation_type.cc b/third_party/blink/renderer/core/animation/css_default_interpolation_type.cc
index c9212564..2f727946 100644
--- a/third_party/blink/renderer/core/animation/css_default_interpolation_type.cc
+++ b/third_party/blink/renderer/core/animation/css_default_interpolation_type.cc
@@ -13,8 +13,9 @@
namespace blink {
CSSDefaultNonInterpolableValue::CSSDefaultNonInterpolableValue(
- const CSSValue* css_value)
- : css_value_(css_value) {
+ const CSSValue* css_value,
+ AttrTainted is_attr_tainted)
+ : css_value_(css_value), is_attr_tainted_(is_attr_tainted) {
DCHECK(css_value_);
}
@@ -49,7 +50,8 @@
return InterpolationValue(
MakeGarbageCollected<InterpolableList>(0),
- MakeGarbageCollected<CSSDefaultNonInterpolableValue>(css_value));
+ MakeGarbageCollected<CSSDefaultNonInterpolableValue>(
+ css_value, CSSDefaultNonInterpolableValue::AttrTainted(false)));
}
void CSSDefaultInterpolationType::Composite(
@@ -66,9 +68,15 @@
CSSInterpolationEnvironment& environment) const {
DCHECK(
To<CSSDefaultNonInterpolableValue>(non_interpolable_value)->CssValue());
+ CSSProperty::ValueMode values_mode =
+ To<CSSDefaultNonInterpolableValue>(non_interpolable_value)
+ ->IsAttrTainted()
+ ? CSSProperty::ValueMode::kAttrTaintedAndAnimated
+ : CSSProperty::ValueMode::kAnimated;
StyleBuilder::ApplyProperty(
GetProperty().GetCSSPropertyName(), environment.GetState(),
- *To<CSSDefaultNonInterpolableValue>(non_interpolable_value)->CssValue());
+ *To<CSSDefaultNonInterpolableValue>(non_interpolable_value)->CssValue(),
+ values_mode);
}
} // namespace blink
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 9223868..6e78560 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
@@ -14,7 +14,9 @@
class CORE_EXPORT CSSDefaultNonInterpolableValue final
: public NonInterpolableValue {
public:
- explicit CSSDefaultNonInterpolableValue(const CSSValue*);
+ using AttrTainted = base::StrongAlias<class AttrTaintedTag, bool>;
+ explicit CSSDefaultNonInterpolableValue(const CSSValue*,
+ AttrTainted is_attr_tainted);
~CSSDefaultNonInterpolableValue() final = default;
void Trace(Visitor* visitor) const override {
@@ -24,10 +26,18 @@
const CSSValue* CssValue() const { return css_value_.Get(); }
+ bool IsAttrTainted() const { return is_attr_tainted_; }
+
DECLARE_NON_INTERPOLABLE_VALUE_TYPE();
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.
+ bool is_attr_tainted_;
};
template <>
diff --git a/third_party/blink/renderer/core/css/properties/css_property.h b/third_party/blink/renderer/core/css/properties/css_property.h
index a50c37f..61e04f52 100644
--- a/third_party/blink/renderer/core/css/properties/css_property.h
+++ b/third_party/blink/renderer/core/css/properties/css_property.h
@@ -314,6 +314,9 @@
kNormal,
// https://drafts.csswg.org/css-variables/#animation-tainted
kAnimated,
+ // https://drafts.csswg.org/css-values-5/#attr-taint
+ kAttrTainted,
+ kAttrTaintedAndAnimated,
};
private:
diff --git a/third_party/blink/renderer/core/css/properties/longhands/custom_property.cc b/third_party/blink/renderer/core/css/properties/longhands/custom_property.cc
index 4cf650e..a3f3919 100644
--- a/third_party/blink/renderer/core/css/properties/longhands/custom_property.cc
+++ b/third_party/blink/renderer/core/css/properties/longhands/custom_property.cc
@@ -204,7 +204,8 @@
return;
}
- bool is_animation_tainted = value_mode == ValueMode::kAnimated;
+ bool is_animation_tainted = value_mode == ValueMode::kAnimated ||
+ value_mode == ValueMode::kAttrTaintedAndAnimated;
// Note that the computed value ("SetVariableValue") is stored separately
// from the substitution value ("SetVariableData") on ComputedStyle.
@@ -212,8 +213,10 @@
// the custom property, and the computed value is generally used in other
// cases (e.g. serialization).
- bool is_attr_tainted = declaration && declaration->VariableDataValue() &&
- declaration->VariableDataValue()->IsAttrTainted();
+ bool is_attr_tainted = value_mode == ValueMode::kAttrTainted ||
+ value_mode == ValueMode::kAttrTaintedAndAnimated ||
+ (declaration && declaration->VariableDataValue() &&
+ declaration->VariableDataValue()->IsAttrTainted());
registered_value = &StyleBuilderConverter::ConvertRegisteredPropertyValue(
state, *registered_value, context);
diff --git a/third_party/blink/web_tests/external/wpt/css/css-values/attr-security-animation.html b/third_party/blink/web_tests/external/wpt/css/css-values/attr-security-animation.html
new file mode 100644
index 0000000..6390b05
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-values/attr-security-animation.html
@@ -0,0 +1,51 @@
+<!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 --s {
+ syntax: "<string>";
+ inherits: false;
+ initial-value: "x";
+ }
+ #attr {
+ --s: attr(href);
Regression Test / PoC
diff --git a/third_party/blink/web_tests/external/wpt/css/css-values/attr-security-animation.html b/third_party/blink/web_tests/external/wpt/css/css-values/attr-security-animation.html
new file mode 100644
index 0000000..6390b05
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-values/attr-security-animation.html
@@ -0,0 +1,51 @@
+<!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 --s {
+ syntax: "<string>";
+ inherits: false;
+ initial-value: "x";
+ }
+ #attr {
+ --s: attr(href);
+ animation: 1s anim linear;
+ background-image: image-set(var(--s));
+ }
+
+ @keyframes anim {
+ from {
+ --s: "x";
+ }
+ to {
+ --s: attr(href);
+ }
+ }
+</style>
+
+<html>
+ <body>
+ <div id="attr" href="https://does-not-exist.test/404.png">div</div>
+ </body>
+</html>
+
+<script>
+ test(() => {
+ // during animation
+ document.getAnimations().forEach(anim => {
+ anim.currentTime = 600;
+ });
+ var elem = document.getElementById("attr");
+ assert_equals(window.getComputedStyle(elem).getPropertyValue("background-image"), 'none');
+
+ // after animation
+ document.getAnimations().forEach(anim => {
+ anim.currentTime = 3000;
+ });
+ var elem = document.getElementById("attr");
+ assert_equals(window.getComputedStyle(elem).getPropertyValue("background-image"), 'none');
+ });
+</script>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-values/attr-security-transition.html b/third_party/blink/web_tests/external/wpt/css/css-values/attr-security-transition.html
new file mode 100644
index 0000000..ad15e56
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-values/attr-security-transition.html
@@ -0,0 +1,34 @@
+<!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 --s {
+ syntax: "<string>";
+ inherits: false;
+ initial-value: "x";
+ }
+ #attr {
+ --s: attr(href);
+ transition: --s 10s allow-discrete -9s;
+ background-image: image-set(var(--s));
+ }
+ @starting-style {
+ #attr { --s: "x"; }
+ }
+</style>
+
+<html>
+ <body>
+ <div id="attr" href="https://does-not-exist.test/404.png">div</div>
+ </body>
+</html>
+
+<script>
+ var elem = document.getElementById("attr");
+ test(() => {
+ assert_equals(window.getComputedStyle(elem).getPropertyValue("background-image"), 'none');
+ }, `background-image value should be attr-tainted`);
+</script>
Original Bug Report
Bypass of attr()-security via discrete 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 without the Chrome Security team.
Overview: A potential vulnerability in Blink’s CSS engine allows bypassing the attr()-security control by using discrete transitions on registered custom properties. The issue occurs because transition endpoints are snapshotted as typed CSS values, which strips the is_attr_tainted flag. This could permit the exfiltration of sensitive attribute values via CSS-only techniques.
Affected files:
third_party/blink/renderer/core/css/properties/longhands/custom_property.ccthird_party/blink/renderer/core/animation/css/css_animations.ccthird_party/blink/renderer/core/css/resolver/style_cascade.ccthird_party/blink/renderer/core/animation/css_default_interpolation_type.ccthird_party/blink/renderer/core/css/css_syntax_definition.ccthird_party/blink/renderer/core/css/properties/css_parsing_utils.ccthird_party/blink/renderer/core/css/css_attr_type.cc
Estimated timestamp from git blame: 2025-04-02
Summary
A potential vulnerability exists in Blink’s CSS implementation that allows for the laundering of the is_attr_tainted flag. This flag is a security mechanism designed to enforce the attr()-security control, preventing values derived from HTML attributes via attr() from being used in sensitive contexts like URLs. By leveraging discrete transitions on registered custom properties, an attacker can strip the taint from a value, bypassing these security checks and potentially exfiltrating sensitive data.
Technical Details
- The
attr()-securitycontrol marks tokens substituted fromattr()as tainted. This boolean flag (is_attr_tainted_) is stored withinCSSVariableData. - When a registered custom property (e.g., with syntax
<string>) is assigned anattr()value,CustomProperty::ApplyValueparses it into a typedCSSValue(likeCSSStringValue).CSSValuedoes not store the taint flag. However,ApplyValuecorrectly creates and stores a parallelCSSVariableDataon theComputedStylewith the taint preserved. - If a CSS transition with
transition-behavior: allow-discreteis triggered on this property, the animation engine calculates endpoints usingAnimationUtils::KeyframeValueFromComputedStyle. This function retrieves the computed typedCSSValue(CSSStringValue), which lacks the taint information. - During the animation,
CSSDefaultInterpolationType::Applypasses this untaintedCSSStringValueback intoCustomProperty::ApplyValue. - Inside
CustomProperty::ApplyValue, the code attempts to cast the value to aCSSUnparsedDeclarationValueto extract the taint:const auto* declaration = DynamicTo<CSSUnparsedDeclarationValue>(value);. - Because the incoming value is a
CSSStringValue, the cast fails (declarationisnullptr), andis_attr_taintedis incorrectly evaluated asfalse. - A new, untainted
CSSVariableDatais created and stored on theComputedStyle. - If this laundered custom property is subsequently used in a sensitive sink via
var()(e.g.,image-set()), the security check incss_parsing_utils::ConsumeImage(stream.IsAttrTainted()) evaluates tofalse, allowing the URL to be fetched.
Suggested Attacker Steps (Note: These are suggested steps; our tooling agent does not yet have the ability to run code to confirm a working exploit).
- Define a registered custom property with
<string>syntax.@property --s { syntax: "<string>"; inherits: false; initial-value: "initial"; } - Apply the custom property using
attr()to target an attribute containing sensitive data (e.g., a CSRF token inhref). - Trigger a discrete transition to launder the value.
a[href] { --s: attr(href); transition: --s 0.001s allow-discrete; background-image: image-set(var(--s) 1x); } @starting-style { a[href] { --s: "initial"; } } - The transition launders the taint, and the browser incorrectly issues a GET request to the attacker’s server with the sensitive attribute data.
Suggested Fix
To resolve this, the taint state (is_attr_tainted) must be preserved when custom properties are resolved into typed CSSValue objects, or the animation system must be updated to retrieve and preserve the original CSSVariableData for registered custom properties when taking keyframe snapshots. Alternatively, CSSValue could be augmented to carry the taint bit, ensuring that it is never lost when converting between unparsed token sequences and typed values.
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.