CVE-2026-17827
Overview
Files Changed
third_party/blink/renderer/core/css/media_query.ccthird_party/blink/renderer/core/css/media_query_exp.ccthird_party/blink/web_tests/external/wpt/css/css-conditional/container-queries/at-container-style-serialization.htmlthird_party/blink/web_tests/external/wpt/css/mediaqueries/mq-escaped-serialization.html
Patch
From 8da6092d0abefc8408bb6d75d3a1f05a90e99079 Mon Sep 17 00:00:00 2001
From: Rune Lillesveen <futhark@chromium.org>
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 <sesse@chromium.org>
Commit-Queue: Rune Lillesveen <futhark@chromium.org>
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>
Regression Test / PoC
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>
Original Bug Report
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.