Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper encoding or escaping of output in CSS
DescriptionImproper encoding or escaping of output in CSS
ComponentCSS
Bug ClassLogic Error
Tracker518082852
Fix commit9ef64552f77b (chromium/src) +10/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

Files Changed

  • third_party/blink/renderer/core/css/cssom/css_unparsed_value.cc
  • third_party/blink/web_tests/external/wpt/css/css-typed-om/stylevalue-serialization/cssUnparsedValue.html
From 9ef64552f77b59e11090cdb22ce4c72592c52ddd Mon Sep 17 00:00:00 2001
From: Rune Lillesveen <[email protected]>
Date: Mon, 03 Aug 2026 03:36:13 -0700
Subject: [PATCH] Escape custom property names for unparsed serialization

Bug: 518082852
Change-Id: Ie31aebb22cd83730d44869f475ff3c1ec37b4078
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8180666
Reviewed-by: Javier Fernandez <[email protected]>
Commit-Queue: Rune Lillesveen <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1672568}
---

diff --git a/third_party/blink/renderer/core/css/cssom/css_unparsed_value.cc b/third_party/blink/renderer/core/css/cssom/css_unparsed_value.cc
index 42b91b5e..d4e0189 100644
--- a/third_party/blink/renderer/core/css/cssom/css_unparsed_value.cc
+++ b/third_party/blink/renderer/core/css/cssom/css_unparsed_value.cc
@@ -5,6 +5,7 @@
 #include "third_party/blink/renderer/core/css/cssom/css_unparsed_value.h"
 
 #include "css_style_value.h"
+#include "third_party/blink/renderer/core/css/css_markup.h"
 #include "third_party/blink/renderer/core/css/css_unparsed_declaration_value.h"
 #include "third_party/blink/renderer/core/css/css_variable_data.h"
 #include "third_party/blink/renderer/core/css/cssom/css_style_variable_reference_value.h"
@@ -244,7 +245,7 @@
         const auto* reference_value =
             segments_[i]->GetAsCSSVariableReferenceValue();
         builder.Append("var(");
-        builder.Append(reference_value->variable());
+        SerializeIdentifier(reference_value->variable(), builder);
         if (reference_value->fallback()) {
           builder.Append(",");
           if (!reference_value->fallback()->AppendUnparsedString(
diff --git a/third_party/blink/web_tests/external/wpt/css/css-typed-om/stylevalue-serialization/cssUnparsedValue.html b/third_party/blink/web_tests/external/wpt/css/css-typed-om/stylevalue-serialization/cssUnparsedValue.html
index 6e9b1a0..691dfb0 100644
--- a/third_party/blink/web_tests/external/wpt/css/css-typed-om/stylevalue-serialization/cssUnparsedValue.html
+++ b/third_party/blink/web_tests/external/wpt/css/css-typed-om/stylevalue-serialization/cssUnparsedValue.html
@@ -8,6 +8,7 @@
 <body>
 <div id="tag"></div>
 <div id="elem" style="color-scheme: bar/*comment*/var(--foo)"></div>
+<div id="customEscaped" style="--a\,fail: pass; --unparsed:var(--a\,fail)"></div>
 <script>
 'use strict';
 
@@ -48,4 +49,11 @@
       'bar/**/var(--foo)');
 }, 'attributeStyleMap round-trips correctly, though the comment is gone');
 
+test(() => {
+  let specified = customEscaped.attributeStyleMap.get("--unparsed");
+  assert_equals(getComputedStyle(customEscaped).getPropertyValue("--unparsed"), "pass");
+  customEscaped.style.setProperty("--unparsed", specified.toString());
+  assert_equals(getComputedStyle(customEscaped).getPropertyValue("--unparsed"), "pass");
+}, 'String serialization of CSSUnparsedValue should roundtrip');
+
 </script>
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/external/wpt/css/css-typed-om/stylevalue-serialization/cssUnparsedValue.html b/third_party/blink/web_tests/external/wpt/css/css-typed-om/stylevalue-serialization/cssUnparsedValue.html
index 6e9b1a0..691dfb0 100644
--- a/third_party/blink/web_tests/external/wpt/css/css-typed-om/stylevalue-serialization/cssUnparsedValue.html
+++ b/third_party/blink/web_tests/external/wpt/css/css-typed-om/stylevalue-serialization/cssUnparsedValue.html
@@ -8,6 +8,7 @@
 <body>
 <div id="tag"></div>
 <div id="elem" style="color-scheme: bar/*comment*/var(--foo)"></div>
+<div id="customEscaped" style="--a\,fail: pass; --unparsed:var(--a\,fail)"></div>
 <script>
 'use strict';
 
@@ -48,4 +49,11 @@
       'bar/**/var(--foo)');
 }, 'attributeStyleMap round-trips correctly, though the comment is gone');
 
+test(() => {
+  let specified = customEscaped.attributeStyleMap.get("--unparsed");
+  assert_equals(getComputedStyle(customEscaped).getPropertyValue("--unparsed"), "pass");
+  customEscaped.style.setProperty("--unparsed", specified.toString());
+  assert_equals(getComputedStyle(customEscaped).getPropertyValue("--unparsed"), "pass");
+}, 'String serialization of CSSUnparsedValue should roundtrip');
+
 </script>
Loading diff…

Original Bug Report

reported by [email protected]

MXSS in CSSUnparsedValue serialization due to missing custom-property name escaping

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 Mutation XSS (mXSS) vulnerability exists in the CSS Typed OM serialization of custom-property variable names. When CSSUnparsedValue.toString() serializes a CSSVariableReferenceValue segment, it emits the decoded identifier name verbatim without performing proper escaping (such as the CSSOM ‘serialize an identifier’ algorithm). This allows unescaped HTML metacharacters to survive in the serialized output, potentially leading to script execution if re-injected into style blocks.

Affected files:

  • third_party/blink/renderer/core/css/cssom/css_unparsed_value.cc
  • third_party/blink/renderer/core/css/cssom/css_style_variable_reference_value.cc

Estimated timestamp from git blame: 2017-12-06

Root Cause

In third_party/blink/renderer/core/css/cssom/css_unparsed_value.cc, the AppendUnparsedString method serializes CSSVariableReferenceValue segments by appending the custom-property variable name raw to the string builder:

case V8CSSUnparsedSegment::ContentType::kCSSVariableReferenceValue: {
  const auto* reference_value =
      segments_[i]->GetAsCSSVariableReferenceValue();
  builder.Append("var(");
  builder.Append(reference_value->variable()); // Missing SerializeIdentifier()
  ...

During parsing of CSS, any CSS escapes inside custom-property identifiers (e.g., \3c for <) are decoded into their literal representation. Thus, the variable name stored in CSSStyleVariableReferenceValue::variable_ can contain literal HTML metacharacters such as < or >. Additionally, the JS constructor for CSSVariableReferenceValue only performs a basic starts_with("--") check on the variable name, allowing developers/attackers to directly supply names with arbitrary characters.

When CSSUnparsedValue.toString() is called, it serializes this raw variable name without utilizing the standard CSSOM SerializeIdentifier algorithm, allowing these metacharacters to be output in raw form.

Potential Trigger Path

Note: These are suggested/potential steps; our tooling agent does not yet have the ability to dynamically run or execute code.

  1. An attacker is able to inject a CSS style containing escaped sequences inside a custom property name, or construct a CSSVariableReferenceValue directly via the JavaScript API: new CSSVariableReferenceValue("--a,</style><script>alert(1)</script>")
  2. A web application or sanitizer reads the custom property using CSS Typed OM: const val = element.attributeStyleMap.get('--x');
  3. The application serializes the value back to a string: const serialized = val.toString(); This outputs: "var(--a,</style><script>alert/**/(1)</script>)"
  4. If the application or sanitizer injects this serialized string back into a <style> block in the DOM, the HTML parser parses </style>, transitions out of the RAWTEXT style state, parses the <script> element, and executes the payload, leading to potential Mutation XSS (mXSS).

Suggested Fix

  1. In third_party/blink/renderer/core/css/cssom/css_unparsed_value.cc, replace: builder.Append(reference_value->variable()); with: SerializeIdentifier(reference_value->variable(), builder);
  2. In third_party/blink/renderer/core/css/cssom/css_style_variable_reference_value.cc, tighten the validation in CSSStyleVariableReferenceValue::Create and setVariable to verify that the variable name conforms to a valid <dashed-ident> token.

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