Visitar URL original
fix: tolerate non-node children when serializing selectors · postcss/postcss-selector-parser@e2021c5 · GitHub
Skip to content

Commit e2021c5

Browse files
committed
fix: tolerate non-node children when serializing selectors
The CVE-2026-9358 fix rewrote Container#toString to recurse through a private _stringify method, which assumes every child implements it. But passing an array to replaceWith() / insertBefore() / insertAfter() stores a raw Array (not a Node) in `nodes`: the previous `this.map(String)` coerced it leniently, whereas the new code threw `TypeError: child._stringify is not a function`. This silently broke Tailwind CSS v3, which uses `pseudo.replaceWith(pseudo.nodes)` to expand :merge() — the mechanism behind every group-* / peer-* variant — producing zero CSS for those variants. Serialization now falls back to String(child) for non-node children, restoring the pre-fix behaviour while keeping the nesting-depth guard intact. Closes #320 Closes #321
1 parent 7893b74 commit e2021c5

4 files changed

Lines changed: 30 additions & 3 deletions

File tree

‎src/__tests__/container.mjs‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,3 +455,17 @@ test("Container#atPosition comma in second selector pseudo", (t) => {
455455
t.deepEqual(node.toString(), ":matches(ol, ul)");
456456
});
457457
});
458+
459+
// Regression for #320/#321/#322: passing an array to replaceWith() stores a
460+
// raw Array (not a Node) inside `nodes`. Serialization must tolerate it, as it
461+
// did before the recursion fix — this is how Tailwind v3 expands `:merge()`
462+
// for every group-* / peer-* variant.
463+
test("Container#toString tolerates a raw array child from replaceWith(array)", (t) => {
464+
const ast = parser().astSync(":merge(.group):hover .foo");
465+
ast.walkPseudos((p) => {
466+
if (p.value === ":merge") {
467+
p.replaceWith(p.nodes); // array passed directly, not spread
468+
}
469+
});
470+
t.deepEqual(ast.toString(), ".group:hover .foo");
471+
});

‎src/selectors/container.js‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,17 @@ export default class Container extends Node {
337337
}
338338

339339
_stringify(options, depth, max) {
340-
return this.map((child) => child._stringify(options, depth, max)).join("");
340+
return this.map((child) => this._stringifyChild(child, options, depth, max)).join("");
341+
}
342+
343+
// Serialize a child node. Historically `toString` used `this.map(String)`,
344+
// which leniently coerced anything — including raw arrays inserted via
345+
// `replaceWith(array)` / `insertBefore` / `insertAfter` (e.g. Tailwind's
346+
// `:merge()` expansion). Fall back to `String(child)` for values that are not
347+
// parser nodes so that behaviour is preserved.
348+
_stringifyChild(child, options, depth, max) {
349+
return typeof child._stringify === "function"
350+
? child._stringify(options, depth, max)
351+
: String(child);
341352
}
342353
}

‎src/selectors/pseudo.js‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ export default class Pseudo extends Container {
1212
throw new Error(`Cannot serialize selector: nesting depth exceeds the maximum of ${max}.`);
1313
}
1414
let params = this.length
15-
? "(" + this.map((child) => child._stringify(options, depth + 1, max)).join(",") + ")"
15+
? "(" +
16+
this.map((child) => this._stringifyChild(child, options, depth + 1, max)).join(",") +
17+
")"
1618
: "";
1719
return [this.rawSpaceBefore, this.stringifyProperty("value"), params, this.rawSpaceAfter].join(
1820
"",

‎src/selectors/root.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export default class Root extends Container {
99

1010
_stringify(options, depth, max) {
1111
let str = this.reduce((memo, selector) => {
12-
memo.push(selector._stringify(options, depth, max));
12+
memo.push(this._stringifyChild(selector, options, depth, max));
1313
return memo;
1414
}, []).join(",");
1515
return this.trailingComma ? str + "," : str;

0 commit comments

Comments
 (0)