Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in CSS
DescriptionInappropriate implementation in CSS
ComponentCSS
Bug ClassLogic Error
Tracker517693726
Fix commit8da6092d0abe (chromium/src) +21/-4
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Files Changed

  • third_party/blink/renderer/core/css/media_query.cc
  • third_party/blink/renderer/core/css/media_query_exp.cc
  • third_party/blink/web_tests/external/wpt/css/css-conditional/container-queries/at-container-style-serialization.html
  • third_party/blink/web_tests/external/wpt/css/mediaqueries/mq-escaped-serialization.html
From 8da6092d0abefc8408bb6d75d3a1f05a90e99079 Mon Sep 17 00:00:00 2001
From: Rune Lillesveen <[email protected]>
Date: Mon, 08 Jun 2026 05:17:58 -0700
Subject: [PATCH] Escape media types and features in serialization

Escaped media features and custom property features were not escaped on
serialization. Use SerializeIdentifier() instead of Append().

Bug: 518112775, 517693726
Change-Id: Ibfa43b9a7d925a3efe65049fb80b8309353a46c4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7900079
Reviewed-by: Steinar H Gunderson <[email protected]>
Commit-Queue: Rune Lillesveen <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1643107}
---

diff --git a/third_party/blink/renderer/core/css/media_query.cc b/third_party/blink/renderer/core/css/media_query.cc
index bf64558..440afd1c 100644
--- a/third_party/blink/renderer/core/css/media_query.cc
+++ b/third_party/blink/renderer/core/css/media_query.cc
@@ -62,7 +62,7 @@
 
   if (MediaType() != media_type_names::kAll ||
       Restrictor() != RestrictorType::kNone) {
-    result.Append(MediaType());
+    SerializeIdentifier(MediaType(), result);
     result.Append(" and ");
   }
 
diff --git a/third_party/blink/renderer/core/css/media_query_exp.cc b/third_party/blink/renderer/core/css/media_query_exp.cc
index 491eaa4..1bba194 100644
--- a/third_party/blink/renderer/core/css/media_query_exp.cc
+++ b/third_party/blink/renderer/core/css/media_query_exp.cc
@@ -659,7 +659,7 @@
   // <mf-plain>  e.g. (width: 100px)
   if (!bounds_.IsRange()) {
     if (HasMediaFeature() || IsCustomMedia()) {
-      result.Append(media_feature_);
+      SerializeIdentifier(media_feature_, result);
     } else {
       result.Append(reference_value_->CssText());
     }
@@ -677,7 +677,7 @@
       result.Append(" ");
     }
     if (HasMediaFeature()) {
-      result.Append(media_feature_);
+      SerializeIdentifier(media_feature_, result);
     } else {
       result.Append(reference_value_->CssText());
     }
diff --git a/third_party/blink/web_tests/external/wpt/css/css-conditional/container-queries/at-container-style-serialization.html b/third_party/blink/web_tests/external/wpt/css/css-conditional/container-queries/at-container-style-serialization.html
index 3e9bd988..6d3a1f5 100644
--- a/third_party/blink/web_tests/external/wpt/css/css-conditional/container-queries/at-container-style-serialization.html
+++ b/third_party/blink/web_tests/external/wpt/css/css-conditional/container-queries/at-container-style-serialization.html
@@ -23,11 +23,13 @@
   @container style(((--FOO: BAR)) and (--bar: foo)) { }
   @container style((((--foo) or ((--bar)))) and ((--baz)) and ((not ((--xyzzy))))) { }
   @container style((((--a: b) or ((b: a)))) and ((--baz)) and ((not ((x: y))))) { }
+  @container style(--\{foo  : bar) { }
+  @container style(100px  > --\{foo >10px) {}
 </style>
 <script>
   setup(() => {
     assert_implements_style_container_queries();
-    assert_equals(testSheet.sheet.cssRules.length, 15);
+    assert_equals(testSheet.sheet.cssRules.length, 17);
   });
 
   const tests = [
@@ -47,6 +49,8 @@
       ["style(((--FOO: BAR)) and (--bar: foo))", "Subexpressions and extra parens"],
       ["style((((--foo) or ((--bar)))) and ((--baz)) and ((not ((--xyzzy)))))", "Multiple subexpressions and extra parens"],
       ["style((((--a: b) or ((b: a)))) and ((--baz)) and ((not ((x: y)))))", "Multiple subexpressions and unknowns"],
+      ["style(--\\{foo: bar)", "Escape custom property identifier"],
+      ["style(100px > --\\{foo > 10px)", "Escape custom property identifier - range syntax"],
   ].map((e, i) => [testSheet.sheet.cssRules[i], ...e]);
 
   tests.forEach((t) => {
diff --git a/third_party/blink/web_tests/external/wpt/css/mediaqueries/mq-escaped-serialization.html b/third_party/blink/web_tests/external/wpt/css/mediaqueries/mq-escaped-serialization.html
new file mode 100644
index 0000000..d5f048ac
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/mediaqueries/mq-escaped-serialization.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<title>Media Queries Test: Serialization of escaped identifiers</title>
+<link rel="help" href="https://drafts.csswg.org/mediaqueries-4/#mq-syntax">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style id="media_sheet">
+  @media \{screen and (--\(FOO: bar) {}
+</style>
+<script>
+  test(() => {
+    assert_equals(media_sheet.sheet.cssRules[0].conditionText, "\\{screen and (--\\(FOO: bar)")
+  }, "Serialization of media query escapes identifiers when necessary");
+</script>
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/external/wpt/css/css-conditional/container-queries/at-container-style-serialization.html b/third_party/blink/web_tests/external/wpt/css/css-conditional/container-queries/at-container-style-serialization.html
index 3e9bd988..6d3a1f5 100644
--- a/third_party/blink/web_tests/external/wpt/css/css-conditional/container-queries/at-container-style-serialization.html
+++ b/third_party/blink/web_tests/external/wpt/css/css-conditional/container-queries/at-container-style-serialization.html
@@ -23,11 +23,13 @@
   @container style(((--FOO: BAR)) and (--bar: foo)) { }
   @container style((((--foo) or ((--bar)))) and ((--baz)) and ((not ((--xyzzy))))) { }
   @container style((((--a: b) or ((b: a)))) and ((--baz)) and ((not ((x: y))))) { }
+  @container style(--\{foo  : bar) { }
+  @container style(100px  > --\{foo >10px) {}
 </style>
 <script>
   setup(() => {
     assert_implements_style_container_queries();
-    assert_equals(testSheet.sheet.cssRules.length, 15);
+    assert_equals(testSheet.sheet.cssRules.length, 17);
   });
 
   const tests = [
@@ -47,6 +49,8 @@
       ["style(((--FOO: BAR)) and (--bar: foo))", "Subexpressions and extra parens"],
       ["style((((--foo) or ((--bar)))) and ((--baz)) and ((not ((--xyzzy)))))", "Multiple subexpressions and extra parens"],
       ["style((((--a: b) or ((b: a)))) and ((--baz)) and ((not ((x: y)))))", "Multiple subexpressions and unknowns"],
+      ["style(--\\{foo: bar)", "Escape custom property identifier"],
+      ["style(100px > --\\{foo > 10px)", "Escape custom property identifier - range syntax"],
   ].map((e, i) => [testSheet.sheet.cssRules[i], ...e]);
 
   tests.forEach((t) => {
diff --git a/third_party/blink/web_tests/external/wpt/css/mediaqueries/mq-escaped-serialization.html b/third_party/blink/web_tests/external/wpt/css/mediaqueries/mq-escaped-serialization.html
new file mode 100644
index 0000000..d5f048ac
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/mediaqueries/mq-escaped-serialization.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<title>Media Queries Test: Serialization of escaped identifiers</title>
+<link rel="help" href="https://drafts.csswg.org/mediaqueries-4/#mq-syntax">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style id="media_sheet">
+  @media \{screen and (--\(FOO: bar) {}
+</style>
+<script>
+  test(() => {
+    assert_equals(media_sheet.sheet.cssRules[0].conditionText, "\\{screen and (--\\(FOO: bar)")
+  }, "Serialization of media query escapes identifiers when necessary");
+</script>
Loading diff…

Original Bug Report

reported by [email protected]

CSS-mXSS via unescaped style query custom-property names in MediaQueryExp::Serialize

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 CSS serialization mechanism in Blink fails to escape custom property names when serializing style container queries, resulting in unescaped rule injection in the CSSOM. An attacker can craft a style query containing escaped characters that parses as a single valid rule but serializes into multiple top-level rules. This breaks the CSSOM round-trip invariant, potentially enabling CSS mutation XSS (CSS-mXSS) in sanitizers and downstream parsers.

Affected files:

  • third_party/blink/renderer/core/css/media_query_exp.cc

Estimated timestamp from git blame: 2022-06-24

Root Cause

In third_party/blink/renderer/core/css/media_query_exp.cc, the MediaQueryExp::Serialize() function appends the stored feature name (media_feature_) verbatim to the output string builder without applying the CSSOM’s serialize an identifier algorithm:

String MediaQueryExp::Serialize() const {
  StringBuilder result;
  if (!bounds_.IsRange()) {
    if (HasMediaFeature() || IsCustomMedia()) {
      result.Append(media_feature_); // Raw append without escaping
    }
  }
  return result.ReleaseString();
}

For style queries, the custom property parsed is processed by StyleFeatureSet, whose only restriction is starting with --. During parsing, escape sequences (e.g., closing brackets or braces) are decoded into raw characters and stored. Upon serialization through web-exposed entry points like CSSContainerRule::cssText(), these stored characters are output unescaped, leading to structural rule injection.

Potential Attack Steps

An attacker can define a style query containing escaped characters that decode to braces and rule-ending delimiters. When serialized, these characters are written raw into the output. For example, a style container query like --a followed by braces and raw CSS will serialize directly, resulting in injected CSS rules. Any downstream sanitizer that parses and then serializes this CSSOM tree will emit rule-injected CSS, bypassing sanitization filters.

Suggested Fix

Import third_party/blink/renderer/core/css/css_markup.h and use SerializeIdentifier(media_feature_, result) instead of result.Append(media_feature_) inside MediaQueryExp::Serialize().

Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379


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