Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInjection in CSS
DescriptionInjection in CSS
ComponentCSS
Bug ClassLogic Error
Tracker518094442
Fix commit7f2437c764a1 (chromium/src) +36/-6
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/core/css/css_syntax_component.cc
modified
test_cssText
third_party/blink/web_tests/external/wpt/css/css-mixins/functions/at-function-cssom.html
modified

Files Changed

  • third_party/blink/renderer/core/css/build.gni
  • third_party/blink/renderer/core/css/css_syntax_component.cc
  • third_party/blink/renderer/core/css/css_syntax_component.h
  • third_party/blink/web_tests/external/wpt/css/css-mixins/functions/at-function-cssom.html
  • third_party/blink/web_tests/external/wpt/css/css-properties-values-api/at-property-cssom.html
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/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/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/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
 
Loading diff…

Regression Test / PoC

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

Original Bug Report

reported by [email protected]

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.h
  • third_party/blink/renderer/core/css/css_syntax_definition.cc
  • third_party/blink/renderer/core/css/css_function_rule.cc
  • third_party/blink/renderer/core/css/css_mixin_rule.cc
  • third_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.

View on issue tracker