← Back to Blog
CVE-2026-41305 · CVSS 6.1 MODERATE

PostCSS ships ~120M npm downloads a week. Before v8.5.10, parse any user CSS, stringify it, drop the result inside a <style> tag, and the string </style> walks straight through. The browser sees a closing tag where a CSS value used to be. Game over.

The PoC is one line of input:

body { content: "</style><script>alert(1)</script><style>"; }

Through postcss.parse and ast.toResult().css, the output is byte-for-byte identical. Wrap it in a <style> block and render: XSS fires. Advisory: GHSA-qx2v-qp2m-jg93.

Why It Slips Through

Two parsers. Two worlds. No shared rulebook.

CSS grammar has no opinion on </style>. Inside a string value it is seven ordinary characters, so PostCSS stores them and writes them back. The HTML tokenizer has exactly one rule for the <style> element: scan forward for the literal bytes </style, then close. Quotes, comments, media queries, none of it is parsed. The terminator is textual.

Put those two rules next to each other and you get a smuggling channel. CSS strings can carry the HTML terminator; PostCSS has no reason to strip it; the downstream template has no idea the string ever contained one.

Not the parser's fault. Not the caller's fault. The contract between them is where the bug lives.

Where It Bites

Vite and webpack generally ship CSS as separate files with Content-Type: text/css. No HTML context, no breakout, not a big deal there.

It bites anywhere CSS ends up inline:

The Patch

v8.5.10 adds one replace to the stringifier:

output = output.replace(/<\/(style)/gi, '<\\/$1');

A backslash inside a CSS string is harmless, but the literal byte sequence </style is gone, so the HTML tokenizer scans past it. Terminator neutralized.

npm ls postcss to find every copy your tree drags in through Autoprefixer, tailwindcss, cssnano, or your bundler. Bump them all.

Takeaway

Parsers are not sanitizers. A CSS parser that round-trips valid CSS is doing its job. A JSON library that preserves </script> is doing its job. An HTML parser that keeps </textarea> inside an attribute is doing its job. The bug is assuming parser output is safe in a context the parser has never heard of.

If you inline CSS, escape </style at the boundary yourself, patched PostCSS or not. Better, serve stylesheets with a <link> and let MIME handle context. Best, pair either with a strict CSP so a breakout becomes a log line instead of a session hijack.

Advisory: GHSA-qx2v-qp2m-jg93 · Patch: postcss v8.5.10