Medium watch CVSS 6.5 chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInjection in CSS
DescriptionInjection in CSS
ComponentCSS
Bug ClassLogic Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker518094442
Fix commit7f2437c764a1 (chromium/src) +36/-6
CWECWE-74
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-30 (per vendor advisory)
Fix landed2026-07-24 (public git)
Disclosed2026-08-25
Patch gap32 days — fix public in source before the advisory

Background

`CSSSyntaxComponent`
a Blink object representing one component of a CSS syntax definition (used by @property descriptors and @function parameter/return types), holding a CSSSyntaxType and, for identifiers, the raw ident string_.
`@function` / `@property` syntax descriptors
CSS features whose syntax can 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.

Key insight
The single mistake was serializing a user-controlled custom identifier into CSS text without escaping it; the fix replaces the raw string_ emission with SerializeIdentifier, guaranteeing a safe, round-trippable token.

Attack Path

  1. Register a syntax with a crafted ident Author CSS defines an @function parameter/return type or an @property syntax descriptor whose custom identifier contains characters requiring escaping (e.g. type(I\ dent)).
  2. Trigger serialization Script reads back the rule’s cssText (or the descriptor value) through the CSSOM, invoking CSSSyntaxComponent::ToString().
  3. 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.
  4. 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

An attacker (any page authoring CSS) gains a CSS-syntax injection / serialization-integrity issue in the renderer: custom identifiers in @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

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

Audit Directions

  • Unescaped identifier serialization
    Search other CSSOM ToString/cssText paths that append raw ident strings via StrCat/Append rather than SerializeIdentifier/SerializeString.
  • Round-trip fidelity for new descriptors
    For 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 serializers
    Watch for serialization logic implemented inline in headers where escaping helpers may be omitted for brevity; compare against the canonical CSS markup helpers.
From 7f2437c764a16e6d807425bb5df5dd61dd919009 Mon Sep 17 00:00:00 2001
From: Rune Lillesveen <futhark@chromium.org>
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 <kbabbitt@microsoft.com>
Commit-Queue: Rune Lillesveen <futhark@chromium.org>
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
 
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 vm...@google.com

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