CVE-2026-13837
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/css/css_counter_value.cc |
modified |
Files Changed
third_party/blink/renderer/core/css/css_counter_content_value.ccthird_party/blink/renderer/core/css/css_counter_value.ccthird_party/blink/web_tests/external/wpt/css/css-content/parsing/content-valid.htmlthird_party/blink/web_tests/external/wpt/css/css-lists/parsing/counter-increment-valid.htmlthird_party/blink/web_tests/external/wpt/css/css-lists/parsing/counter-reset-valid.htmlthird_party/blink/web_tests/external/wpt/css/css-lists/parsing/counter-set-valid.html
Patch
From 9dbc492937175529d97379c8474945256539faf9 Mon Sep 17 00:00:00 2001 From: Rune Lillesveen <[email protected]> Date: Tue, 19 May 2026 10:30:59 -0700 Subject: [PATCH] Escaped idents when serializing CSS counter names There was missing escaping for idents when serializing counter names in: - The counter-set/reset/increment property values - The content property value Bug: 514429130 Change-Id: Ia21161017622af951fb6895bb995af396489f04e Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7859556 Reviewed-by: Daniil Sakhapov <[email protected]> Commit-Queue: Rune Lillesveen <[email protected]> Cr-Commit-Position: refs/heads/main@{#1632982} --- diff --git a/third_party/blink/renderer/core/css/css_counter_content_value.cc b/third_party/blink/renderer/core/css/css_counter_content_value.cc index 48e13cd..0769edc0 100644 --- a/third_party/blink/renderer/core/css/css_counter_content_value.cc +++ b/third_party/blink/renderer/core/css/css_counter_content_value.cc @@ -19,7 +19,7 @@ result.Append("counters("); } - result.Append(Identifier()); + SerializeIdentifier(Identifier(), result); if (!Separator().empty()) { result.Append(", "); result.Append(separator_->CssText()); diff --git a/third_party/blink/renderer/core/css/css_counter_value.cc b/third_party/blink/renderer/core/css/css_counter_value.cc index 3dbb9a7..3344d21 100644 --- a/third_party/blink/renderer/core/css/css_counter_value.cc +++ b/third_party/blink/renderer/core/css/css_counter_value.cc @@ -4,6 +4,7 @@ #include "third_party/blink/renderer/core/css/css_counter_value.h" +#include "third_party/blink/renderer/core/css/css_markup.h" #include "third_party/blink/renderer/core/css/css_primitive_value.h" #include "third_party/blink/renderer/platform/wtf/text/string_builder.h" @@ -14,7 +15,7 @@ if (is_reversed_) { result.Append("reversed("); } - result.Append(identifier_->Value()); + SerializeIdentifier(identifier_->Value(), result); if (is_reversed_) { result.Append(')'); } diff --git a/third_party/blink/web_tests/external/wpt/css/css-content/parsing/content-valid.html b/third_party/blink/web_tests/external/wpt/css/css-content/parsing/content-valid.html index 097bf66..80bb54f 100644 --- a/third_party/blink/web_tests/external/wpt/css/css-content/parsing/content-valid.html +++ b/third_party/blink/web_tests/external/wpt/css/css-content/parsing/content-valid.html @@ -47,6 +47,9 @@ test_valid_value_combinations("content", `"(" counters(counter-name, ".", counter-style) ")"`); test_valid_value_combinations("content", `open-quote "hello" "world" close-quote`); test_valid_value_combinations("content", `url("picture.svg") "hello"`); + +test_valid_value_combinations("content", "counter(\\})"); +test_valid_value_combinations("content", `counters(\\}, ".")`); </script> </body> </html> diff --git a/third_party/blink/web_tests/external/wpt/css/css-lists/parsing/counter-increment-valid.html b/third_party/blink/web_tests/external/wpt/css/css-lists/parsing/counter-increment-valid.html index 01876223..557290f 100644 --- a/third_party/blink/web_tests/external/wpt/css/css-lists/parsing/counter-increment-valid.html +++ b/third_party/blink/web_tests/external/wpt/css/css-lists/parsing/counter-increment-valid.html @@ -20,6 +20,7 @@ test_valid_value('counter-increment', 'section calc(10 + (5 * sign(2cqw - 10px)))'); test_valid_value('counter-increment', 'a 1 b 2 c 3 d 4 e 5'); test_valid_value('counter-increment', 'a 1 b 2 c 3 a 4 b 5'); +test_valid_value('counter-increment', 'a\\ 8 9'); </script> </body> </html> diff --git a/third_party/blink/web_tests/external/wpt/css/css-lists/parsing/counter-reset-valid.html b/third_party/blink/web_tests/external/wpt/css/css-lists/parsing/counter-reset-valid.html index 4e46291..55c9f5e 100644 --- a/third_party/blink/web_tests/external/wpt/css/css-lists/parsing/counter-reset-valid.html +++ b/third_party/blink/web_tests/external/wpt/css/css-lists/parsing/counter-reset-valid.html @@ -26,6 +26,7 @@ test_valid_value('counter-reset', 'section calc(10 + (5 * sign(2cqw - 10px)))'); test_valid_value('counter-reset', 'a 1 b 2 c 3 d 4 e 5'); test_valid_value('counter-reset', 'a 1 b 2 c 3 a 4 b 5'); +test_valid_value('counter-reset', 'a\\ 8 9'); </script> </body> </html> diff --git a/third_party/blink/web_tests/external/wpt/css/css-lists/parsing/counter-set-valid.html b/third_party/blink/web_tests/external/wpt/css/css-lists/parsing/counter-set-valid.html index f7f1c66..e985fca 100644 --- a/third_party/blink/web_tests/external/wpt/css/css-lists/parsing/counter-set-valid.html +++ b/third_party/blink/web_tests/external/wpt/css/css-lists/parsing/counter-set-valid.html @@ -20,6 +20,7 @@ test_valid_value('counter-set', 'section calc(10 + (5 * sign(2cqw - 10px)))'); test_valid_value('counter-set', 'a 1 b 2 c 3 d 4 e 5'); test_valid_value('counter-set', 'a 1 b 2 c 3 a 4 b 5'); +test_valid_value('counter-set', 'a\\ 8 9'); </script> </body> </html>
Regression Test / PoC
diff --git a/third_party/blink/web_tests/external/wpt/css/css-content/parsing/content-valid.html b/third_party/blink/web_tests/external/wpt/css/css-content/parsing/content-valid.html
index 097bf66..80bb54f 100644
--- a/third_party/blink/web_tests/external/wpt/css/css-content/parsing/content-valid.html
+++ b/third_party/blink/web_tests/external/wpt/css/css-content/parsing/content-valid.html
@@ -47,6 +47,9 @@
test_valid_value_combinations("content", `"(" counters(counter-name, ".", counter-style) ")"`);
test_valid_value_combinations("content", `open-quote "hello" "world" close-quote`);
test_valid_value_combinations("content", `url("picture.svg") "hello"`);
+
+test_valid_value_combinations("content", "counter(\\})");
+test_valid_value_combinations("content", `counters(\\}, ".")`);
</script>
</body>
</html>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-lists/parsing/counter-increment-valid.html b/third_party/blink/web_tests/external/wpt/css/css-lists/parsing/counter-increment-valid.html
index 01876223..557290f 100644
--- a/third_party/blink/web_tests/external/wpt/css/css-lists/parsing/counter-increment-valid.html
+++ b/third_party/blink/web_tests/external/wpt/css/css-lists/parsing/counter-increment-valid.html
@@ -20,6 +20,7 @@
test_valid_value('counter-increment', 'section calc(10 + (5 * sign(2cqw - 10px)))');
test_valid_value('counter-increment', 'a 1 b 2 c 3 d 4 e 5');
test_valid_value('counter-increment', 'a 1 b 2 c 3 a 4 b 5');
+test_valid_value('counter-increment', 'a\\ 8 9');
</script>
</body>
</html>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-lists/parsing/counter-reset-valid.html b/third_party/blink/web_tests/external/wpt/css/css-lists/parsing/counter-reset-valid.html
index 4e46291..55c9f5e 100644
--- a/third_party/blink/web_tests/external/wpt/css/css-lists/parsing/counter-reset-valid.html
+++ b/third_party/blink/web_tests/external/wpt/css/css-lists/parsing/counter-reset-valid.html
@@ -26,6 +26,7 @@
test_valid_value('counter-reset', 'section calc(10 + (5 * sign(2cqw - 10px)))');
test_valid_value('counter-reset', 'a 1 b 2 c 3 d 4 e 5');
test_valid_value('counter-reset', 'a 1 b 2 c 3 a 4 b 5');
+test_valid_value('counter-reset', 'a\\ 8 9');
</script>
</body>
</html>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-lists/parsing/counter-set-valid.html b/third_party/blink/web_tests/external/wpt/css/css-lists/parsing/counter-set-valid.html
index f7f1c66..e985fca 100644
--- a/third_party/blink/web_tests/external/wpt/css/css-lists/parsing/counter-set-valid.html
+++ b/third_party/blink/web_tests/external/wpt/css/css-lists/parsing/counter-set-valid.html
@@ -20,6 +20,7 @@
test_valid_value('counter-set', 'section calc(10 + (5 * sign(2cqw - 10px)))');
test_valid_value('counter-set', 'a 1 b 2 c 3 d 4 e 5');
test_valid_value('counter-set', 'a 1 b 2 c 3 a 4 b 5');
+test_valid_value('counter-set', 'a\\ 8 9');
</script>
</body>
</html>
Original Bug Report
Potential CSS Injection via unescaped identifiers in CSSCounterValue serialization
Flapjack, 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: Blink’s CSSOM serialization for counter values fails to escape custom identifiers, returning raw, unescaped strings. If a web application reads and dynamically re-injects this serialized CSS, an attacker can smuggle structural characters to inject arbitrary CSS rules. This breaks the CSSOM round-trip invariant, creating a potential mutation XSS (mXSS) vulnerability.
Affected files:
third_party/blink/renderer/core/css/css_counter_value.ccthird_party/blink/renderer/core/css/css_counter_content_value.cc
Estimated timestamp from git blame: Unknown (Google3 checkout)
Overview
The CSSOM serialization logic for CSSCounterValue and CSSCounterContentValue in Blink’s CSS engine contains a potential vulnerability that can lead to CSS injection, often referred to as mutation XSS (mXSS).
Specifically, the CustomCSSText() methods in these classes append the raw, decoded identifier string to the serialization buffer instead of its properly escaped CSS representation. This allows structural CSS characters (like ;, {, and }) that were originally safely escaped in the input to be serialized as literal characters, breaking the CSSOM round-trip invariant.
Technical Details
In third_party/blink/renderer/core/css/css_counter_value.cc:
String CSSCounterValue::CustomCSSText() const {
StringBuilder result;
// ...
result.Append(identifier_->Value()); // Vulnerability: appends raw, unescaped string
// ...
}
A parallel issue exists in third_party/blink/renderer/core/css/css_counter_content_value.cc:
String CSSCounterContentValue::CustomCSSText() const {
// ...
result.Append(Identifier()); // Identifier() returns identifier_->Value()
// ...
}
The identifier_ member is a CSSCustomIdentValue. During parsing, any CSS escape sequences (e.g., \3b for ;) are decoded, and the literal string is stored. When CSSCustomIdentValue::CustomCSSText() is called, it correctly uses SerializeIdentifier to re-escape these characters. However, the counter value classes bypass this and call Value(), directly embedding the unescaped string into the output.
Potential Attack Scenario
(Note: These are potential steps to trigger the issue, as our tooling agent does not run active code execution to provide a working PoC.)
- Injection: An attacker provides a payload where a safe CSS property value contains escaped structural characters. For example:
counter-reset: \3b \7d * \7b color: red; \7d 1;(Decodes to:; } * { color: red; } 1;) - Parsing: The browser parses this safely. The tokenizer decodes the escapes into a single identifier token, isolating the attacker’s payload as a valid counter name.
- Serialization: A benign script reads the CSS text via the CSSOM (e.g.,
element.style.cssTextorelement.style.counterReset). Due to the bug, the browser serializes this as:counter-reset: ; } * { color: red; } 1; - Re-parsing / Execution: The application dynamically injects this serialized string back into the DOM (a common pattern in CSS-in-JS libraries, SSR rehydration, or HTML sanitizers). Because the output is no longer escaped, the literal
;prematurely terminates thecounter-resetdeclaration, the}closes the current rule block, and* { color: red; }is evaluated as a completely new, attacker-controlled CSS rule.
This grants the attacker the ability to inject arbitrary CSS, which can be leveraged for UI spoofing or data exfiltration via CSS-based side channels.
Suggested Fix
Update the CustomCSSText() implementations in both affected classes to rely on the underlying identifier’s safe serialization method rather than extracting the raw value.
For CSSCounterValue:
Change result.Append(identifier_->Value()); to result.Append(identifier_->CustomCSSText());
For CSSCounterContentValue:
Change result.Append(Identifier()); to result.Append(identifier_->CustomCSSText());
Evaluated with Chrome root at commit: b7d0c4d810da1b31400f198c70d9720fc8f0e5a0
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.