CVE-2026-17845
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
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.ccthird_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:
- When
exp_node == nullptr: The media type is correctly escaped usingSerializeIdentifier(line 59). - When
exp_node != nullptr: The unescapedMediaType()is appended directly to the output string builder using rawresult.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).
- 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){}'); - 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)`. - 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.
Recommended Fix
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.