How Path Traversal Happens in Node.js PostCSS and How to Fix It
The Problem: Your CSS Processor Was Reading Files It Shouldn't
The package-lock.json file in this project locked PostCSS at version 8.5.8 — a version containing a high-severity path traversal flaw tracked as GHSA-r28c-9q8g-f849. PostCSS is one of the most downloaded npm packages in existence, used by webpack, Vite, Next.js, and countless build pipelines to transform CSS. A flaw in how it auto-loads previous source maps means that crafted CSS input can trick the library into reading arbitrary .map files off the server's filesystem.
This isn't a theoretical edge case. Any pipeline that processes CSS from an untrusted source — user uploads, third-party stylesheets fetched at build time, or plugin-generated CSS — is potentially exposed.
The Vulnerability Explained
How PostCSS Loads Previous Source Maps
When PostCSS processes a CSS file, it looks for a sourceMappingURL comment at the bottom of the file, like this:
/* Standard sourceMappingURL comment */
/*# sourceMappingURL=styles.css.map */
This is a standard feature: PostCSS reads the referenced .map file from disk so it can chain source maps correctly across multiple transformation passes. The problem in versions before 8.5.18 is that the path in the sourceMappingURL comment was not properly sanitized before being used to read a file.
The Vulnerable Pattern
Consider what happens when an attacker supplies a CSS file containing:
body { color: red; }
/*# sourceMappingURL=../../../../etc/passwd.map */
Or, targeting source maps that may exist alongside sensitive configuration files:
/*# sourceMappingURL=../../../config/database.js.map */
PostCSS's previous-source-map auto-loading code would resolve this path relative to the CSS file's location and attempt to read it from disk. If the file exists (and .map files are the target), its contents would be loaded into memory and potentially exposed through PostCSS's output or error messages.
The vulnerable version pinned in package-lock.json before this fix:
"node_modules/postcss": {
"version": "8.5.8",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="
}
Real-World Attack Scenario
Imagine a web application that accepts CSS file uploads for a "custom theme" feature and processes them through PostCSS at build time or on a background worker. An attacker uploads:
.theme { background: #fff; }
/*# sourceMappingURL=../../../../app/config/secrets.js.map */
If a .map file exists at that traversed path — perhaps generated by a prior build step that compiled secrets.js — PostCSS silently reads it. Depending on how the application surfaces PostCSS errors or outputs, the attacker may be able to exfiltrate source map contents, which often contain original source code, variable names, and logic that was intended to be minified and obscured.
Even without a secrets file, source maps for application JavaScript frequently contain reconstructed source that reveals business logic, API endpoints, and authentication flows.
The Fix
What Changed
The fix involved two coordinated changes across package.json and package-lock.json.
1. Upgrading the resolved PostCSS version in package-lock.json:
"node_modules/postcss": {
- "version": "8.5.8",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
- "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
+ "version": "8.5.18",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.18.tgz",
+ "integrity": "sha512-xdB1oSLHbz1vRWgCDalrCqEFTWzFlhqFC5tIHLMOSUIjhm3XXQ1qrFy8S/ESr1JYRRXqM3c1QFiMZUJdUTqyMQ==",
2. Adding an npm overrides entry to both package.json and package-lock.json:
+ "overrides": {
+ "postcss": "^8.5.18"
+ }
This second change is critical and often overlooked. Because PostCSS appears in this project as a peer dependency (note the "peer": true flag in package-lock.json) of @mermaid-js/mermaid-cli and mermaid, simply updating the top-level version isn't sufficient — npm might still resolve the vulnerable 8.5.8 version for nested dependents. The overrides field forces npm to use ^8.5.18 for every installation of PostCSS in the entire dependency tree, regardless of what the consuming package requested.
3. The nanoid sub-dependency was also bumped:
- "nanoid": "^3.3.11",
+ "nanoid": "^3.3.12",
PostCSS 8.5.18 tightened its own dependency on nanoid as part of the same security hardening pass.
Why the overrides Approach Matters
Without the overrides entry, a future npm install or npm update could silently re-introduce the vulnerable version if mermaid-cli or mermaid still declare a loose peer dependency range that resolves to 8.5.8. The override acts as a security floor — a guarantee that no matter what the dependency graph looks like, PostCSS will always be at least 8.5.18.
Prevention & Best Practices
1. Treat sourceMappingURL as Untrusted Input
If your application processes CSS from any external source, treat the sourceMappingURL comment as attacker-controlled data. Validate and sanitize it before passing the CSS to PostCSS, or disable previous source map auto-loading if your use case doesn't require it.
2. Use npm overrides (or Yarn resolutions) for Transitive Vulnerabilities
When a vulnerable package is a transitive dependency — meaning you don't depend on it directly — the overrides field is your most reliable tool:
{
"overrides": {
"postcss": "^8.5.18"
}
}
For Yarn users, the equivalent is:
{
"resolutions": {
"postcss": "^8.5.18"
}
}
3. Run Dependency Audits in CI
Integrate npm audit, Trivy, or Snyk into your CI pipeline so vulnerable transitive dependencies are caught before they reach production. This vulnerability was detected by Trivy scanning package-lock.json — the kind of check that should run on every pull request.
4. Pin Integrity Hashes
The integrity field in package-lock.json (the sha512 hash) ensures that even if a registry is compromised, npm will refuse to install a package whose content doesn't match the expected hash. Always commit your package-lock.json.
5. Reference Security Standards
- OWASP: This falls under A01:2021 – Broken Access Control and the Path Traversal attack category.
- CWE-22: Improper Limitation of a Pathname to a Restricted Directory.
- Always validate that resolved file paths begin with the expected base directory using
path.resolve()and a prefix check:
const path = require('path');
function safeResolvePath(baseDir, userInput) {
const resolved = path.resolve(baseDir, userInput);
if (!resolved.startsWith(path.resolve(baseDir) + path.sep)) {
throw new Error('Path traversal detected');
}
return resolved;
}
Key Takeaways
sourceMappingURLcomments in CSS are attacker-controlled data — PostCSS 8.5.8 trusted them too much when auto-loading previous source maps, enabling directory traversal.- Peer dependencies need explicit overrides — because PostCSS was a peer dep of
mermaid-cli, a simple version bump wasn't enough; theoverridesentry inpackage.jsonwas required to enforce the safe version across the full tree. .mapfiles can contain sensitive source code — source maps reconstruct original JavaScript, making them a high-value target even when the original files are not directly accessible.- Trivy scanning
package-lock.jsoncaught this before exploitation — static analysis of lockfiles is a fast, low-friction way to surface known-vulnerable transitive dependencies. - The
nanoidbump to^3.3.12was a bundled fix — PostCSS 8.5.18 tightened multiple sub-dependencies simultaneously, reinforcing the value of upgrading to the latest patch rather than the minimum fix version.
How Orbis AppSec Detected This
- Source: A
sourceMappingURLcomment embedded in CSS input processed by PostCSS, providing an attacker-controlled file path string. - Sink: PostCSS's internal previous-source-map auto-loading logic, which reads the file referenced by
sourceMappingURLfrom disk — effectively an unsanitizedfs.readFile()call derived from CSS content. - Missing control: No path normalization or directory-boundary check was applied to the
sourceMappingURLvalue before it was used to construct the file path, allowing../sequences to escape the intended base directory. - CWE: CWE-22 — Improper Limitation of a Pathname to a Restricted Directory ("Path Traversal").
- Fix: PostCSS was upgraded from
8.5.8to8.5.18inpackage-lock.json, and anoverridesentry was added topackage.jsonto enforce this minimum version across all transitive dependency resolutions.
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
GHSA-r28c-9q8g-f849 is a reminder that even well-established, widely-trusted build tools can harbor path traversal bugs in features that seem benign — like loading a source map. The sourceMappingURL auto-loading mechanism in PostCSS 8.5.8 trusted user-controlled CSS content to provide a safe file path, and it didn't. The fix in 8.5.18 corrects the path resolution logic, and the overrides entry in this project ensures the safe version is enforced for every consumer in the dependency tree.
If your project uses PostCSS — directly or through a bundler like webpack or Vite — check your package-lock.json now. If you see 8.5.8 anywhere in the resolved versions, add the overrides entry and re-run npm install. It's a two-line change that closes a meaningful attack surface.