Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in CSS
DescriptionInappropriate implementation in CSS
ComponentCSS
Bug ClassLogic Error
Tracker514420555
Fix commitb6089ec4b04e (chromium/src) +18/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Files Changed

  • third_party/blink/renderer/core/css/css_position_try_rule.cc
  • third_party/blink/renderer/core/css/css_position_try_rule.h
  • third_party/blink/web_tests/external/wpt/css/css-anchor-position/at-position-try-cssom.html
From b6089ec4b04eceb7d6249a25c30b9495ccdb4606 Mon Sep 17 00:00:00 2001
From: Rune Lillesveen <futhark@chromium.org>
Date: Tue, 19 May 2026 06:05:15 -0700
Subject: [PATCH] Escape 'name' part of @position-try prelude when necessary

Bug: 514420555
Change-Id: I97ba58938a16c52f47f9e8003ace9f98ede207f5
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7859393
Reviewed-by: Morten Stenshorne <mstensho@chromium.org>
Commit-Queue: Rune Lillesveen <futhark@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1632802}
---

diff --git a/third_party/blink/renderer/core/css/css_position_try_rule.cc b/third_party/blink/renderer/core/css/css_position_try_rule.cc
index 26f91b40..323e56a7 100644
--- a/third_party/blink/renderer/core/css/css_position_try_rule.cc
+++ b/third_party/blink/renderer/core/css/css_position_try_rule.cc
@@ -4,6 +4,7 @@
 
 #include "third_party/blink/renderer/core/css/css_position_try_rule.h"
 
+#include "third_party/blink/renderer/core/css/css_markup.h"
 #include "third_party/blink/renderer/core/css/css_position_try_descriptors.h"
 #include "third_party/blink/renderer/core/css/css_property_value_set.h"
 #include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
@@ -27,10 +28,16 @@
 
 CSSPositionTryRule::~CSSPositionTryRule() = default;
 
+String CSSPositionTryRule::name() const {
+  StringBuilder result;
+  SerializeIdentifier(position_try_rule_->Name(), result);
+  return result.ReleaseString();
+}
+
 String CSSPositionTryRule::cssText() const {
   StringBuilder result;
   result.Append("@position-try ");
-  result.Append(name());
+  SerializeIdentifier(position_try_rule_->Name(), result);
   result.Append(" { ");
   if (!position_try_rule_->Properties().IsEmpty()) {
     result.Append(position_try_rule_->Properties().AsText());
diff --git a/third_party/blink/renderer/core/css/css_position_try_rule.h b/third_party/blink/renderer/core/css/css_position_try_rule.h
index d07cca2..22e073c 100644
--- a/third_party/blink/renderer/core/css/css_position_try_rule.h
+++ b/third_party/blink/renderer/core/css/css_position_try_rule.h
@@ -50,7 +50,7 @@
   CSSStyleDeclaration* style() const;
   Type GetType() const final { return kPositionTryRule; }
 
-  String name() const { return position_try_rule_->Name(); }
+  String name() const;
   String cssText() const final;
   void Reattach(StyleRuleBase*) final;
 
diff --git a/third_party/blink/web_tests/external/wpt/css/css-anchor-position/at-position-try-cssom.html b/third_party/blink/web_tests/external/wpt/css/css-anchor-position/at-position-try-cssom.html
index 126b9fa8..f15fa762 100644
--- a/third_party/blink/web_tests/external/wpt/css/css-anchor-position/at-position-try-cssom.html
+++ b/third_party/blink/web_tests/external/wpt/css/css-anchor-position/at-position-try-cssom.html
@@ -30,6 +30,15 @@
 }, 'CSSPositionTryRule attribute values');
 
 test(t => {
+  const style = createStyle(
+      t, '@position-try --\\{ {}');
+  const positionTryRule = style.sheet.cssRules[0];
+  assert_true(positionTryRule instanceof CSSPositionTryRule);
+  assert_equals(positionTryRule.name, '--\\{');
+  assert_equals(positionTryRule.cssText, '@position-try --\\{ { }');
+}, 'CSSPositionTryRule escaped ident name');
+
+test(t => {
   const style = createStyle(t, `
     @position-try --pf { top: anchor(top); left: 0; }
     #anchor, #other-anchor, #target {
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/external/wpt/css/css-anchor-position/at-position-try-cssom.html b/third_party/blink/web_tests/external/wpt/css/css-anchor-position/at-position-try-cssom.html
index 126b9fa8..f15fa762 100644
--- a/third_party/blink/web_tests/external/wpt/css/css-anchor-position/at-position-try-cssom.html
+++ b/third_party/blink/web_tests/external/wpt/css/css-anchor-position/at-position-try-cssom.html
@@ -30,6 +30,15 @@
 }, 'CSSPositionTryRule attribute values');
 
 test(t => {
+  const style = createStyle(
+      t, '@position-try --\\{ {}');
+  const positionTryRule = style.sheet.cssRules[0];
+  assert_true(positionTryRule instanceof CSSPositionTryRule);
+  assert_equals(positionTryRule.name, '--\\{');
+  assert_equals(positionTryRule.cssText, '@position-try --\\{ { }');
+}, 'CSSPositionTryRule escaped ident name');
+
+test(t => {
   const style = createStyle(t, `
     @position-try --pf { top: anchor(top); left: 0; }
     #anchor, #other-anchor, #target {
Loading diff…

Original Bug Report

reported by rj...@google.com

Potential mXSS via unescaped @position-try rule name 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: The CSSOM serialization logic for @position-try rules in Blink fails to escape the rule name, outputting it raw. If an attacker uses CSS escapes to embed HTML tags (like </style>) into the rule name, the serialized cssText can trigger a mutation XSS (mXSS) when injected back into the DOM.

Affected files:

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

Estimated timestamp from git blame: Unknown (Google3 checkout)

The implementation of CSSPositionTryRule::cssText() in third_party/blink/renderer/core/css/css_position_try_rule.cc incorrectly appends the rule’s name directly to the output StringBuilder without proper CSS escaping.

During CSS parsing, escape sequences within identifiers (such as \3c for < or \2f for /) are decoded and stored internally as literal characters. To prevent these characters from breaking out of their CSS context when serialized back to text, the CSSOM specification requires them to be safely re-escaped. While other rules (such as @keyframes in CSSKeyframesRule::cssText()) correctly invoke SerializeIdentifier() to escape special characters, @position-try rules append the raw, unescaped string directly:

// third_party/blink/renderer/core/css/css_position_try_rule.cc
String CSSPositionTryRule::cssText() const {
  StringBuilder result;
  result.Append("@position-try ");
  result.Append(name()); // <--- Vulnerability: Raw string appended without SerializeIdentifier()
  result.Append(" { ");
  ...

Potential Attack Steps

(Note: These are suggested steps based on static analysis; our tooling agent does not currently have the ability to run code to verify a working proof-of-concept.)

  1. Inject Malicious CSS: An attacker provides malicious CSS to a vulnerable web application, utilizing CSS escapes within a @position-try rule name: @position-try --foo\3c \2fstyle\3e \3c script\3e alert(1)\3c \2fscript\3e { margin: 10px; }
  2. Browser Parsing: The browser’s CSS parser unescapes the rule name and stores it internally as the literal string --foo</style><script>alert(1)</script>.
  3. CSSOM Read: A script running on the page, such as a DOM sanitizer (e.g., DOMPurify) or an application dynamically copying styles, reads the rule’s .cssText property.
  4. Vulnerable Serialization: Because SerializeIdentifier() is missing, the browser returns the raw, unescaped string: @position-try --foo</style><script>alert(1)</script> { margin: 10px; }
  5. mXSS Trigger: The script trusts this output (as it originated from the browser’s CSSOM) and injects it back into the DOM inside a <style> block (e.g., styleElement.innerHTML = text;).
  6. Code Execution: The HTML parser encounters the unescaped </style> sequence, prematurely terminating the style block, and executes the subsequent <script> tag, resulting in a Cross-Site Scripting (XSS) vulnerability.

Suggested Fix

Update CSSPositionTryRule::cssText() in third_party/blink/renderer/core/css/css_position_try_rule.cc to safely serialize the identifier:

#include "third_party/blink/renderer/core/css/css_markup.h"
// ... 
String CSSPositionTryRule::cssText() const {
  StringBuilder result;
  result.Append("@position-try ");
  SerializeIdentifier(name(), result); // <--- FIX
  result.Append(" { ");
// ...

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.

View on issue tracker