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
Tracker514441966
Fix commitcb7a65383327 (chromium/src) +21/-11
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Files Changed

  • third_party/blink/renderer/core/css/css_selector.cc
  • third_party/blink/renderer/core/css/parser/css_selector_parser.cc
  • third_party/blink/web_tests/external/wpt/css/css-forms/parsing/picker-select-pseudo-element-expected.txt
  • third_party/blink/web_tests/external/wpt/css/cssom/getComputedStyle-pseudo-picker-expected.txt
From cb7a65383327b369a1a9b54fafe7edef2d6b3b75 Mon Sep 17 00:00:00 2001
From: Joey Arhar <[email protected]>
Date: Wed, 03 Jun 2026 14:18:22 -0700
Subject: [PATCH] Improve ::picker(select) CSS parsing

This patch restricts the parsing of ::picker() to only accept
::picker(select) and prevents a potential round-tripping issue when
serializing ::picker() for CSSOM.

Fixed: 514441966
Change-Id: I8d96e4d1af51b3d31d2d5c4d7d25ee58b6eeb620
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7877455
Reviewed-by: Rune Lillesveen <[email protected]>
Reviewed-by: Joey Arhar <[email protected]>
Commit-Queue: Joey Arhar <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1641204}
---

diff --git a/third_party/blink/renderer/core/css/css_selector.cc b/third_party/blink/renderer/core/css/css_selector.cc
index 6cd5fa2..0097ae6 100644
--- a/third_party/blink/renderer/core/css/css_selector.cc
+++ b/third_party/blink/renderer/core/css/css_selector.cc
@@ -1401,13 +1401,13 @@
         builder.Append(')');
         break;
       }
+      case kPseudoPicker:
       case kPseudoHighlight: {
         builder.Append('(');
         SerializeIdentifier(Argument(), builder);
         builder.Append(')');
         break;
       }
-      case kPseudoPicker:
       case kPseudoOverscrollAreaParent:
       case kPseudoScrollButton: {
         builder.Append('(');
diff --git a/third_party/blink/renderer/core/css/parser/css_selector_parser.cc b/third_party/blink/renderer/core/css/parser/css_selector_parser.cc
index eb674b8..7db5cc7 100644
--- a/third_party/blink/renderer/core/css/parser/css_selector_parser.cc
+++ b/third_party/blink/renderer/core/css/parser/css_selector_parser.cc
@@ -1763,7 +1763,25 @@
       output_.push_back(std::move(selector));
       return true;
     }
-    case CSSSelector::kPseudoPicker:
+    case CSSSelector::kPseudoPicker: {
+      const CSSParserToken& ident = stream.Peek();
+      if (ident.GetType() != kIdentToken) {
+        return false;
+      }
+      // If we add more valid arguments to ::picker() in the future, then we
+      // would probably have to turn this into a list instead of just checking
+      // for "select".
+      if (!EqualIgnoringAsciiCase(ident.Value(), "select")) {
+        return false;
+      }
+      selector.SetArgument(AtomicString("select"));
+      stream.ConsumeIncludingWhitespace();
+      if (!stream.AtEnd()) {
+        return false;
+      }
+      output_.push_back(std::move(selector));
+      return true;
+    }
     case CSSSelector::kPseudoDir:
     case CSSSelector::kPseudoState: {
       const CSSParserToken& ident = stream.Peek();
diff --git a/third_party/blink/web_tests/external/wpt/css/css-forms/parsing/picker-select-pseudo-element-expected.txt b/third_party/blink/web_tests/external/wpt/css/css-forms/parsing/picker-select-pseudo-element-expected.txt
deleted file mode 100644
index 24e53c76..0000000
--- a/third_party/blink/web_tests/external/wpt/css/css-forms/parsing/picker-select-pseudo-element-expected.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-This is a testharness.js-based test.
-[FAIL] "::picker(foo)" should be an invalid selector
-  assert_throws_dom: "::picker(foo)" should throw in querySelector function "() => document.querySelector(selector)" did not throw
-[FAIL] "::picker(SELECT)" should be a valid selector
-  assert_equals: serialization should be canonical expected "::picker(select)" but got "::picker(SELECT)"
-Harness: the test ran to completion.
-
diff --git a/third_party/blink/web_tests/external/wpt/css/cssom/getComputedStyle-pseudo-picker-expected.txt b/third_party/blink/web_tests/external/wpt/css/cssom/getComputedStyle-pseudo-picker-expected.txt
index d21bebb..e8f34b9 100644
--- a/third_party/blink/web_tests/external/wpt/css/cssom/getComputedStyle-pseudo-picker-expected.txt
+++ b/third_party/blink/web_tests/external/wpt/css/cssom/getComputedStyle-pseudo-picker-expected.txt
@@ -1,6 +1,5 @@
 This is a testharness.js-based test.
 [FAIL] ::picker(select) on non-select element resolves the style
   assert_equals: expected "rgb(0, 0, 255)" but got "rgba(0, 0, 0, 0)"
-[FAIL] Invalid pseudo-element should return empty style: ::picker(div)
-  assert_equals: expected 0 but got 487
 Harness: the test ran to completion.
+
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/external/wpt/css/css-forms/parsing/picker-select-pseudo-element-expected.txt b/third_party/blink/web_tests/external/wpt/css/css-forms/parsing/picker-select-pseudo-element-expected.txt
deleted file mode 100644
index 24e53c76..0000000
--- a/third_party/blink/web_tests/external/wpt/css/css-forms/parsing/picker-select-pseudo-element-expected.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-This is a testharness.js-based test.
-[FAIL] "::picker(foo)" should be an invalid selector
-  assert_throws_dom: "::picker(foo)" should throw in querySelector function "() => document.querySelector(selector)" did not throw
-[FAIL] "::picker(SELECT)" should be a valid selector
-  assert_equals: serialization should be canonical expected "::picker(select)" but got "::picker(SELECT)"
-Harness: the test ran to completion.
-
diff --git a/third_party/blink/web_tests/external/wpt/css/cssom/getComputedStyle-pseudo-picker-expected.txt b/third_party/blink/web_tests/external/wpt/css/cssom/getComputedStyle-pseudo-picker-expected.txt
index d21bebb..e8f34b9 100644
--- a/third_party/blink/web_tests/external/wpt/css/cssom/getComputedStyle-pseudo-picker-expected.txt
+++ b/third_party/blink/web_tests/external/wpt/css/cssom/getComputedStyle-pseudo-picker-expected.txt
@@ -1,6 +1,5 @@
 This is a testharness.js-based test.
 [FAIL] ::picker(select) on non-select element resolves the style
   assert_equals: expected "rgb(0, 0, 255)" but got "rgba(0, 0, 0, 0)"
-[FAIL] Invalid pseudo-element should return empty style: ::picker(div)
-  assert_equals: expected 0 but got 487
 Harness: the test ran to completion.
+
Loading diff…

Original Bug Report

reported by [email protected]

CSS Injection (mXSS) via unescaped ::picker() pseudo-element 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 ::picker() pseudo-element fails to escape its argument during CSS serialization. If a web application parses and reflects this serialized CSS, an attacker can use CSS escape sequences to inject arbitrary CSS rules. This could lead to Mutation Cross-Site Scripting (mXSS), allowing for data exfiltration or UI redressing.

Affected files:

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

Estimated timestamp from git blame: 2026-03-31

Vulnerability Details

A potential CSS mutation vulnerability (mXSS) exists in the serialization logic for the ::picker() pseudo-element within third_party/blink/renderer/core/css/css_selector.cc.

When the Blink CSS parser processes a ::picker() functional pseudo-element, it decodes any CSS escape sequences in its argument and stores the unescaped raw string internally. However, during CSS Object Model (CSSOM) serialization (e.g., when a script reads cssRule.cssText or selectorText), the CSSSelector::SerializeSimpleSelector function appends the argument directly to the output buffer without re-escaping it.

      case kPseudoPicker:
      case kPseudoOverscrollAreaParent:
      case kPseudoScrollButton: {
        builder.Append('(');
        // These accept only fixed arguments that do not require escaping (in
        // some cases including "*" which should not be escaped).
        builder.Append(Argument()); // Bug: Raw string appended without escaping
        builder.Append(')');
        break;
      }

Unlike other pseudo-classes (such as :dir() or :state()) which correctly use SerializeIdentifier(Argument(), builder), the raw concatenation here allows an attacker to break out of the pseudo-element syntax if the serialized CSS is later reflected into a <style> tag by a web application.

Potential Steps to Trigger

Note: These are suggested steps based on code analysis; our tooling agent does not currently have the capability to run a live proof-of-concept to verify execution in a full browser environment.

  1. Payload Injection: An attacker provides a malicious CSS string to a vulnerable web application that parses, serializes, and later reflects CSS. The payload uses CSS escape sequences to smuggle syntax characters: div::picker(\29 \7b \7d * \7b color\3a red\3b \7d ) {}
  2. Internal Decoding: The parser processes the argument, unescaping \29 to ), \7b to {, etc. The internal token value becomes ) {} * { color: red; }.
  3. Serialization: The application accesses the CSSOM (e.g., rule.cssText). SerializeSimpleSelector emits the unescaped string, generating: div::picker() {} * { color: red; }) {}.
  4. Reparsing and Execution: The application writes the serialized string back to the DOM. The browser parses it as three segments:
    • div::picker() {} (Syntactically valid rule)
    • * { color: red; } (Injected arbitrary CSS rule)
    • ) {} (Safely ignored via parser error recovery)

Through this CSS injection, an attacker could potentially achieve UI redressing or data exfiltration (e.g., using attribute selectors to read and leak sensitive CSRF tokens or user data to an external server).

Suggested Fix

Update CSSSelector::SerializeSimpleSelector in third_party/blink/renderer/core/css/css_selector.cc.

The safest approach is to handle kPseudoPicker by utilizing SerializeIdentifier(Argument(), builder) instead of directly appending Argument(), ensuring that any special characters are correctly escaped according to CSS serialization rules (similar to how kPseudoState and kPseudoDir are handled).

Alternatively, if ::picker() is only ever meant to accept a fixed internal keyword (e.g., select), the parser (CSSSelectorParser::ConsumePseudo) should be updated to strictly reject arbitrary identifiers and ensure the argument is exactly from the allowed keyword set.

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