CVE-2026-79234
Overview
Background
- `CSSSyntaxComponent`
- a Blink object representing one component of a CSS
syntaxdefinition (used by@propertydescriptors and@functionparameter/return types), holding aCSSSyntaxTypeand, for identifiers, the raw identstring_. - `@function` / `@property` syntax descriptors
- CSS features whose
syntaxcan name custom identifiers (e.g.type(I\ dent)), which the CSSOM must be able to serialize back to text. - `SerializeIdentifier`
- the Blink CSS markup helper that escapes an arbitrary string into a valid, safely-quoted CSS identifier token.
- `ToString()` serialization
- the round-trip that produces
cssText; if it emits an unescaped ident, the re-parsed text no longer represents the same token.
Root Cause Analysis
CSSSyntaxComponent::ToString() builds the textual form of a syntax component and, for type_ == CSSSyntaxType::kIdent, previously emitted the raw string_ directly via StrCat with no escaping. Custom identifiers can legally contain characters that are only representable when escaped (whitespace, control characters, delimiters), so an unescaped ident serialized into a syntax descriptor produced cssText that, when re-parsed, no longer round-tripped to the same identifier — a CSS injection / serialization-integrity flaw.
The fix moves ToString() into css_syntax_component.cc and, for the kIdent case, routes the string through SerializeIdentifier(string_, builder) so the emitted token is properly escaped, while non-ident types still use blink::ToString(type_). The added WPT cases (@function --identy(--a I\ dent) returns type(I\ dent) and the @property I\ dent syntax) confirm that identifiers with an escaped space now serialize back to their escaped form. This restores the invariant that CSSOM serialization of a syntax component always yields text that re-parses to the identical identifier.
string_ emission with SerializeIdentifier, guaranteeing a safe, round-trippable token.Attack Path
- Register a syntax with a crafted ident
Author CSS defines an
@functionparameter/return type or an@propertysyntaxdescriptor whose custom identifier contains characters requiring escaping (e.g.type(I\ dent)). - Trigger serialization
Script reads back the rule’s
cssText(or the descriptor value) through the CSSOM, invokingCSSSyntaxComponent::ToString(). - Unescaped emission
The pre-patch code emits the raw
string_, producing CSS text where the identifier’s special characters are not escaped, so the serialized descriptor no longer represents the original token. - Injection on re-parse When that serialized text is re-parsed or reinserted, the unescaped characters change token boundaries/structure, allowing the identifier content to bleed into surrounding CSS syntax (injection) (inferred downstream effect of the broken round-trip).
Impact Assessment
@function/@property syntax descriptors could be serialized without escaping, so a CSSOM round-trip no longer faithfully reproduced the token. The advisory classifies this as a logic error / injection in CSS; no exploited-in-the-wild status or terminal escalation is stated. Preconditions are only that a page defines such a syntax descriptor and its cssText is serialized.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/css/css_syntax_component.cc |
modified | |
test_cssTextthird_party/blink/web_tests/external/wpt/css/css-mixins/functions/at-function-cssom.html |
modified |
Files Changed
third_party/blink/renderer/core/css/build.gnithird_party/blink/web_tests/external/wpt/css/css-mixins/functions/at-function-cssom.htmlthird_party/blink/renderer/core/css/css_syntax_component.hthird_party/blink/web_tests/external/wpt/css/css-properties-values-api/at-property-cssom.htmlthird_party/blink/renderer/core/css/css_syntax_component.cc
Audit Directions
- Unescaped identifier serializationSearch other CSSOM
ToString/cssTextpaths that append raw ident strings viaStrCat/Appendrather thanSerializeIdentifier/SerializeString. - Round-trip fidelity for new descriptorsFor newly added at-rules and descriptors (
@function,@property, custom identifiers), verify parse→serialize→parse produces identical tokens, especially for whitespace/control-character escapes. - Header-inlined serializersWatch for serialization logic implemented inline in headers where escaping helpers may be omitted for brevity; compare against the canonical CSS markup helpers.
Patch
From 7f2437c764a16e6d807425bb5df5dd61dd919009 Mon Sep 17 00:00:00 2001 From: Rune Lillesveen <[email protected]> Date: Fri, 24 Jul 2026 11:29:35 -0700 Subject: [PATCH] Escape CSS syntax component identifiers Missing escape of custom identifiers used in syntax definition for @function parameter and return types. Also added a test for escaped identifiers in @property syntax descriptors. Bug: 518094442 Change-Id: Idf41c16dccabdcdf3350e2eb5dc3fa58e16e0f7b Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8143363 Reviewed-by: Kevin Babbitt <[email protected]> Commit-Queue: Rune Lillesveen <[email protected]> Cr-Commit-Position: refs/heads/main@{#1668036} --- diff --git a/third_party/blink/renderer/core/css/css_syntax_component.cc b/third_party/blink/renderer/core/css/css_syntax_component.cc new file mode 100644 index 0000000..e3eac3f --- /dev/null +++ b/third_party/blink/renderer/core/css/css_syntax_component.cc @@ -0,0 +1,23 @@ +// Copyright 2026 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "third_party/blink/renderer/core/css/css_syntax_component.h" + +#include "third_party/blink/renderer/core/css/css_markup.h" +#include "third_party/blink/renderer/platform/wtf/text/string_builder.h" + +namespace blink { + +String CSSSyntaxComponent::ToString() const { + StringBuilder builder; + if (type_ == CSSSyntaxType::kIdent) { + SerializeIdentifier(string_, builder); + } else { + builder.Append(blink::ToString(type_)); + } + builder.Append(blink::ToString(repeat_)); + return builder.ReleaseString(); +} + +} // namespace blink diff --git a/third_party/blink/renderer/core/css/css_syntax_component.h b/third_party/blink/renderer/core/css/css_syntax_component.h index 7c4aa1e..d90fcfa 100644 --- a/third_party/blink/renderer/core/css/css_syntax_component.h +++ b/third_party/blink/renderer/core/css/css_syntax_component.h @@ -7,7 +7,6 @@ #include "third_party/blink/renderer/core/css/css_value.h" #include "third_party/blink/renderer/platform/wtf/allocator/allocator.h" -#include "third_party/blink/renderer/platform/wtf/text/strcat.h" #include "third_party/blink/renderer/platform/wtf/text/wtf_string.h" namespace blink { @@ -105,11 +104,7 @@ DCHECK(IsRepeatable()); return repeat_ == CSSSyntaxRepeat::kSpaceSeparated ? ' ' : ','; } - String ToString() const { - String result = - (type_ == CSSSyntaxType::kIdent) ? string_ : blink::ToString(type_); - return StrCat({result, blink::ToString(repeat_)}); - } + String ToString() const; private: CSSSyntaxType type_; diff --git a/third_party/blink/renderer/core/css/build.gni b/third_party/blink/renderer/core/css/build.gni index bbc8218..882c75115 100644 --- a/third_party/blink/renderer/core/css/build.gni +++ b/third_party/blink/renderer/core/css/build.gni @@ -347,6 +347,7 @@ "css_superellipse_value.h", "css_supports_rule.cc", "css_supports_rule.h", + "css_syntax_component.cc", "css_syntax_component.h", "css_syntax_definition.cc", "css_syntax_definition.h", diff --git a/third_party/blink/web_tests/external/wpt/css/css-mixins/functions/at-function-cssom.html b/third_party/blink/web_tests/external/wpt/css/css-mixins/functions/at-function-cssom.html index dbf157b..f967885 100644 --- a/third_party/blink/web_tests/external/wpt/css/css-mixins/functions/at-function-cssom.html +++ b/third_party/blink/web_tests/external/wpt/css/css-mixins/functions/at-function-cssom.html @@ -326,4 +326,9 @@ // Escapes (U+0009 CHARACTER TABULATION): test_cssText(`@function --escaped-\\9 -tab(--param-\\9 -tab) { --local-\\9 -tab: 1px; }`); + +// Escapes ident in syntax +test_cssText(`@function --identy(--a I\\ dent) returns type(I\\ dent) { }`, + `@function --identy(--a I\\ dent) returns I\\ dent { }`); + </script> diff --git a/third_party/blink/web_tests/external/wpt/css/css-properties-values-api/at-property-cssom.html b/third_party/blink/web_tests/external/wpt/css/css-properties-values-api/at-property-cssom.html index 27041c2..d9593db 100644 --- a/third_party/blink/web_tests/external/wpt/css/css-properties-values-api/at-property-cssom.html +++ b/third_party/blink/web_tests/external/wpt/css/css-properties-values-api/at-property-cssom.html @@ -60,6 +60,11 @@ syntax: "*"; inherits: true; } + @property --escape-syntax { + syntax: "I\\ dent|none"; + inherits: false; + initial-value: I\ dent; + } </style> <script> @@ -140,6 +145,7 @@ test_css_text('--no-initial-universal-value', '@property --no-initial-universal-value { syntax: "*"; inherits: false; }'); test_css_text('--tab\ttab', '@property --tab\\9 tab { syntax: "*"; inherits: true; }'); +test_css_text('--escape-syntax', '@property --escape-syntax { syntax: "I\\\\ dent|none"; inherits: false; initial-value: I\\ dent; }'); // CSSRule.type
Regression Test / PoC
diff --git a/third_party/blink/web_tests/external/wpt/css/css-mixins/functions/at-function-cssom.html b/third_party/blink/web_tests/external/wpt/css/css-mixins/functions/at-function-cssom.html
index dbf157b..f967885 100644
--- a/third_party/blink/web_tests/external/wpt/css/css-mixins/functions/at-function-cssom.html
+++ b/third_party/blink/web_tests/external/wpt/css/css-mixins/functions/at-function-cssom.html
@@ -326,4 +326,9 @@
// Escapes (U+0009 CHARACTER TABULATION):
test_cssText(`@function --escaped-\\9 -tab(--param-\\9 -tab) { --local-\\9 -tab: 1px; }`);
+
+// Escapes ident in syntax
+test_cssText(`@function --identy(--a I\\ dent) returns type(I\\ dent) { }`,
+ `@function --identy(--a I\\ dent) returns I\\ dent { }`);
+
</script>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-properties-values-api/at-property-cssom.html b/third_party/blink/web_tests/external/wpt/css/css-properties-values-api/at-property-cssom.html
index 27041c2..d9593db 100644
--- a/third_party/blink/web_tests/external/wpt/css/css-properties-values-api/at-property-cssom.html
+++ b/third_party/blink/web_tests/external/wpt/css/css-properties-values-api/at-property-cssom.html
@@ -60,6 +60,11 @@
syntax: "*";
inherits: true;
}
+ @property --escape-syntax {
+ syntax: "I\\ dent|none";
+ inherits: false;
+ initial-value: I\ dent;
+ }
</style>
<script>
@@ -140,6 +145,7 @@
test_css_text('--no-initial-universal-value', '@property --no-initial-universal-value { syntax: "*"; inherits: false; }');
test_css_text('--tab\ttab', '@property --tab\\9 tab { syntax: "*"; inherits: true; }');
+test_css_text('--escape-syntax', '@property --escape-syntax { syntax: "I\\\\ dent|none"; inherits: false; initial-value: I\\ dent; }');
// CSSRule.type
Original Bug Report
CSSOM Rule Injection via Missing Identifier Serialization in CSSSyntaxComponent::ToString
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: The CSSOM serialization in Blink potentially fails to escape identifier tokens in CSSSyntaxComponent::ToString(), outputting raw decoded characters instead of using SerializeIdentifier(). This can allow an attacker to use CSS escape sequences to construct a parsed identifier containing metacharacters like parentheses or braces, which when serialized via cssText, could break out of the rule block and inject arbitrary top-level CSS rules.
Affected files:
third_party/blink/renderer/core/css/css_syntax_component.hthird_party/blink/renderer/core/css/css_syntax_definition.ccthird_party/blink/renderer/core/css/css_function_rule.ccthird_party/blink/renderer/core/css/css_mixin_rule.ccthird_party/blink/renderer/core/css/css_syntax_string_parser.cc
Estimated timestamp from git blame: 2024-10-28
Potential CSSOM Rule Injection via Missing Identifier Serialization in CSSSyntaxComponent::ToString
Root Cause
In third_party/blink/renderer/core/css/css_syntax_component.h, CSSSyntaxComponent::ToString() is used during serialization to serialize individual components of a syntax definition. When the component’s type is CSSSyntaxType::kIdent, it directly returns the string_ member verbatim instead of applying the standard CSSOM identifier serialization algorithm:
String ToString() const {
String result =
(type_ == CSSSyntaxType::kIdent) ? string_ : blink::ToString(type_);
return StrCat({result, blink::ToString(repeat_)});
}
The correct primitive to use is SerializeIdentifier(), implemented in third_party/blink/renderer/core/css/css_markup.cc, which escapes non-name code points and metacharacters. Since raw decoded strings are emitted directly into the serialized text, this breaks the invariant parse(serialize(x)) == x.
Why string_ Can Contain CSS Metacharacters
When parsing CSS, the tokenizer decodes escape sequences during the construction of an identifier. For example, ConsumeName() unconditionally appends decoded characters parsed via ConsumeEscape() in third_party/blink/renderer/core/css/parser/css_parser_idioms.cc. Therefore, an input identifier containing escaped metacharacters like a\\29 b is tokenized into a single identifier token with a decoded value of a)b.
This decoded value is stored directly in the <syntax> definition string or component via CSSSyntaxDefinition::Consume() or the string parser path in css_syntax_string_parser.cc.
Potential Sinks
Serialized CSS syntax components are used during the serialization of @function and @mixin rules. Specifically, CSSFunctionRule::cssText() embeds CSSSyntaxDefinition::ToString() into the rule-prelude CSS text via AppendCSSType():
void AppendCSSType(const CSSSyntaxDefinition& syntax, StringBuilder& builder) {
CHECK(!syntax.IsUniversal());
bool wrap_in_type = syntax.Components().size() != 1u;
if (wrap_in_type) { builder.Append("type("); }
builder.Append(syntax.ToString()); // <-- Raw decoded identifier lands here
if (wrap_in_type) { builder.Append(")"); }
}
This function is called when serializing both parameter types and return types of @function and @mixin rules. Because the @function feature (CSSFunctions) is stable and enabled by default, this code path is fully web-reachable without flags.
Potential Impact
Because of this serialization flaw, an attacker could potentially construct a stylesheet containing escaped characters that parse safely as a single identifier. When that stylesheet is subsequently serialized via rule.cssText (a common behavior in client-side or CSSOM-based HTML/CSS sanitizers that re-serialize parsed sheets), the output will contain unescaped metacharacters. This can break out of the @function or @mixin rule context and insert arbitrary new top-level rules (such as arbitrary style rules with exfiltration url() beacons, @keyframes, or @font-face definitions) that the sanitizer never validated.
Note: The following steps and proof of concept are potential vectors. Our tooling agent does not have the capability to run code to confirm execution in a live browser.
Potential Steps to Reproduce / Proof of Concept
If the following script is executed in the browser:
const s = new CSSStyleSheet();
s.replaceSync(
'@function --f(--p a\\29 \\7B \\7D \\2A\\7B background\\3A url\\28\\2F\\2F evil\\2F\\29\\7D x) { result: 1px; }'
);
console.log(s.cssRules[0].cssText);
Observed Output:
@function --f(--p a){}*{background:url(//evil/)}x) { result: 1px; }
When re-serialized, the string contains an unescaped closing parenthesis and braces that close the @function block early, inserting *{background:url(//evil/)} as a brand new top-level style rule.
Suggested Fix
To fix this vulnerability, CSSSyntaxComponent::ToString() should serialize identifiers using SerializeIdentifier when type_ == CSSSyntaxType::kIdent:
String ToString() const {
String result;
if (type_ == CSSSyntaxType::kIdent) {
StringBuilder builder;
SerializeIdentifier(string_, builder);
result = builder.ReleaseString();
} else {
result = blink::ToString(type_);
}
return StrCat({result, blink::ToString(repeat_)});
}
Evaluated with Chrome root at commit: fb72408a8493c46bc75fae1c70d03daec96b3040
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.