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
Tracker518112775
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]

MediaQuery::Serialize fails to escape media type when conditional expression is present

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 serialization logic in MediaQuery::Serialize fails to escape media types when serializing a media query that has a conditional expression. An attacker can construct a media type containing escaped characters that decode into CSS metacharacters, which are then output raw during serialization. If a web application parses, serializes, and subsequently re-injects the resulting CSS, this can lead to arbitrary CSS injection.

Affected files:

  • third_party/blink/renderer/core/css/media_query.cc
  • third_party/blink/renderer/core/css/parser/media_query_parser.cc

Estimated timestamp from git blame: 2026-03-30

Summary

A potential vulnerability has been identified in Blink’s CSSOM serialization logic. When a media query contains both a media type and a conditional expression node (e.g., <type> and <condition>), MediaQuery::Serialize() fails to escape the media type string. This allows decoded, unescaped identifier strings containing CSS metacharacters (such as { or }) to be serialized raw. If a web application retrieves the serialized text and later re-injects it (such as inside a client-side sanitizer, dynamic style modifier, or CSSOM-to-CSS generator), it can lead to arbitrary CSS injection and Mutation XSS (mXSS).

Root Cause

In third_party/blink/renderer/core/css/media_query.cc, the MediaQuery::Serialize() method has two branches to output the media type:

  1. When exp_node == nullptr: The media type is correctly escaped using SerializeIdentifier (line 59).
  2. When exp_node != nullptr: The unescaped MediaType() is appended directly to the output string builder using raw result.Append() (line 65).
// third_party/blink/renderer/core/css/media_query.cc
const ConditionalExpNode* exp_node = ExpNode();

if (!exp_node) {
  SerializeIdentifier(MediaType(), result);   // Escaped correctly
  return result.ReleaseString();
}

if (MediaType() != media_type_names::kAll ||
    Restrictor() != RestrictorType::kNone) {
  result.Append(MediaType());                 // <--- Unescaped raw append
  result.Append(" and ");
}

When parsing the initial CSS rule, the tokenizer decodes escape sequences. An identifier token specified as \7d\2a\7b\63olor\3a\72\65\64\7d decodes into the literal string }*{color:red}. This bypasses initial restriction checks because it is parsed as a single identifier token. However, because the serialization phase lacks the corresponding escaping logic for the conditional branch, the literal } and { characters are serialized raw into the CSSOM output.

Potential Impact

If a web application reads the serialized rule (cssText, mediaText, or conditionText) and re-injects it (e.g., using CSSStyleSheet.replaceSync() or direct stylesheet modification), the literal } character closes the existing media rule block prematurely. Any trailing characters will then be parsed as a new top-level style rule, allowing arbitrary CSS injection. While this does not cause memory corruption, it can lead to UI spoofing or data exfiltration via attribute-selector and background-image side channels in affected applications.

Suggested Reproduction Steps

(Note: These are potential steps based on source code analysis; our tooling has not run this code to verify).

  1. Parse a stylesheet containing escaped characters in a conditional media query:
    const s = new CSSStyleSheet();
    s.replaceSync('@media \\7d\\2a\\7b\\63olor\\3a\\72\\65\\64\\7d and (color){}');
    
  2. Retrieve the serialized text:
    const serialized = s.cssRules[0].cssText;
    // The expected output should contain a properly escaped identifier.
    // Instead, the potential output contains the unescaped literal `}*{color:red} and (color)`.
    
  3. Re-inject the serialized CSS:
    otherSheet.replaceSync('body{}' + serialized);
    // The unescaped `}` closes the first rule prematurely, and `*{color:red}` is parsed as a top-level universal rule.
    

Ensure that the media type is serialized using SerializeIdentifier in all branches of MediaQuery::Serialize().

// third_party/blink/renderer/core/css/media_query.cc
if (MediaType() != media_type_names::kAll ||
    Restrictor() != RestrictorType::kNone) {
  SerializeIdentifier(MediaType(), result);
  result.Append(" and ");
}

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