The Vulnerability at a Glance
| Field | Detail |
|---|---|
| Vulnerability | Path Traversal in sourceMappingURL auto-loading |
| CWE | CWE-22 – Improper Limitation of a Pathname to a Restricted Directory |
| Language | JavaScript / Node.js |
| Risk | Arbitrary .map file disclosure from the server filesystem |
| Root Cause | PostCSS did not sanitize path traversal sequences in sourceMappingURL before resolving the referenced file |
| Fix | Upgrade PostCSS from 8.5.15 to 8.5.18; pin via pnpm overrides |
Introduction
The pnpm-lock.yaml file in this project pins PostCSS at version 8.5.15 as a transitive dependency of @vue/cli-plugin-babel, @vue/cli-plugin-eslint, and @vue/cli-plugin-typescript. PostCSS's job is straightforward: parse, transform, and serialize CSS. As part of that pipeline, it can automatically load previous source maps referenced by sourceMappingURL comments embedded in CSS files. But in versions before 8.5.18, PostCSS failed to validate whether the path embedded in that comment stayed within the expected directory — creating a path traversal primitive that could expose arbitrary .map files from the server's filesystem.
This is GHSA-r28c-9q8g-f849, rated HIGH severity. The fix is a targeted version upgrade enforced through a pnpm override so that every package in the dependency tree that pulls in PostCSS gets the patched build.
The Vulnerability Explained
What is sourceMappingURL Auto-Loading?
When PostCSS processes a CSS file, it looks for a comment like this at the end:
/* # sourceMappingURL=styles.css.map */
This tells the toolchain where to find the source map for the file — useful for debugging transpiled or minified CSS. PostCSS can automatically load that map when it parses the file, so subsequent transforms can preserve accurate source positions.
The problem: PostCSS used the value of sourceMappingURL as a file path without sanitizing path traversal sequences.
The Vulnerable Pattern
Before the fix (PostCSS 8.5.15), the source map auto-loading logic would resolve a path like:
/* # sourceMappingURL=../../etc/passwd.map */
or more practically:
/* # sourceMappingURL=../../../app/dist/server.js.map */
PostCSS would dutifully resolve that path relative to the CSS file's location and attempt to read it from disk. There was no check confirming that the resolved absolute path remained within the project's expected output directory.
How an Attacker Could Exploit This
Consider a build pipeline or a server-side CSS processing endpoint that:
- Accepts CSS content from an external source (e.g., a user-uploaded stylesheet, a third-party CSS bundle fetched from a URL, or content passed through an API).
- Passes that CSS through PostCSS for transformation (autoprefixing, minification, etc.).
- Returns or logs the PostCSS output, which may include loaded source map data.
An attacker crafts a CSS file containing:
body { color: red; }
/* # sourceMappingURL=../../../../secrets/build-metadata.js.map */
PostCSS auto-loads ../../../../secrets/build-metadata.js.map, and the map's contents — which may include original source code paths, environment variable names embedded in build tooling, or internal module structure — become accessible to the attacker through the processing result or error output.
Even in a pure build-time context, if the build system processes CSS from untrusted repositories (e.g., in a CI pipeline that builds third-party packages), a malicious sourceMappingURL in a dependency's CSS could read .map files from sensitive locations on the build agent.
Real-World Impact for This Project
In this Vue.js project, the affected packages are:
@vue/cli-plugin-babel@5.0.9(previously resolved againstpostcss@8.5.15)@vue/cli-plugin-eslint@5.0.9(previously resolved againstpostcss@8.5.15)@vue/cli-plugin-typescript@5.0.9(previously resolved againstpostcss@8.5.15)
The assessment notes the vulnerability is present in the dependency tree but not confirmed reachable — meaning there is no direct code path in this application today that passes untrusted CSS through PostCSS's source map loader. However, the primitive exists in the installed code, and future changes to the project (adding a CSS processing endpoint, upgrading Vue CLI plugins, or integrating a new build plugin) could activate it without any obvious security review trigger.
The Fix
Strategy: pnpm Overrides
Because PostCSS is a transitive dependency — not declared directly in dependencies or devDependencies — a simple npm install postcss@8.5.18 would not guarantee that @vue/cli-plugin-* packages use the patched version. They pin their own peer dependency ranges, and the lock file would continue resolving the old version for those packages.
The correct approach for pnpm is a package-level override, which forces every package in the dependency tree that requires postcss to receive version 8.5.18 regardless of what range they specify.
Changes in package.json
Before:
{
"devDependencies": {
"webpack": "^5.73.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.9.3"
}
}
After:
{
"devDependencies": {
"webpack": "^5.73.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.9.3"
},
"pnpm": {
"overrides": {
"postcss": "8.5.18"
}
}
}
The pnpm.overrides block tells pnpm's resolver: no matter what version of PostCSS any package in this tree requests, install exactly 8.5.18.
Changes in pnpm-lock.yaml
The lock file reflects the override at the top level:
overrides:
postcss: 8.5.18
And all resolved peer dependency strings for the affected Vue CLI plugins change from postcss@8.5.15 to postcss@8.5.18:
Before:
'@vue/cli-plugin-babel':
specifier: ^5.0.8
version: 5.0.9(...)(postcss@8.5.15)(...)
'@vue/cli-plugin-eslint':
specifier: ^5.0.8
version: 5.0.9(...)(postcss@8.5.15)(...)
After:
'@vue/cli-plugin-babel':
specifier: ^5.0.8
version: 5.0.9(...)(postcss@8.5.18)(...)
'@vue/cli-plugin-eslint':
specifier: ^5.0.8
version: 5.0.9(...)(postcss@8.5.18)(...)
What PostCSS 8.5.18 Actually Changes
The patch in PostCSS 8.5.18 tightens the path resolution logic for sourceMappingURL values. Before reading any referenced .map file, the resolved absolute path is validated to confirm it does not escape the base directory of the CSS file being processed. Any sourceMappingURL value containing traversal sequences (../, encoded variants, or absolute paths pointing outside the allowed scope) is rejected, and the auto-loading is skipped safely.
This change is backward-compatible: valid sourceMappingURL values pointing to map files within the expected directory continue to work exactly as before.
Prevention & Best Practices
1. Always Validate Paths Before File I/O
The canonical defense against path traversal in Node.js is to resolve the full absolute path and assert it starts with the expected base directory:
const path = require('path');
function safeResolve(baseDir, userInput) {
const resolved = path.resolve(baseDir, userInput);
if (!resolved.startsWith(path.resolve(baseDir) + path.sep)) {
throw new Error('Path traversal detected');
}
return resolved;
}
PostCSS 8.5.18 applies exactly this pattern to sourceMappingURL resolution.
2. Use Package Manager Overrides for Transitive Dependencies
When a vulnerability exists in a transitive dependency, don't rely on indirect updates propagating through the tree. Use your package manager's override mechanism explicitly:
- pnpm:
"pnpm": { "overrides": { "package": "version" } }inpackage.json - npm:
"overrides": { "package": "version" }inpackage.json - yarn:
"resolutions": { "package": "version" }inpackage.json
3. Audit Your Dependency Tree Regularly
The Trivy scanner that detected this vulnerability works by scanning lock files for known-vulnerable package versions. Integrate it into your CI pipeline:
trivy fs --scanners vuln pnpm-lock.yaml
This catches vulnerabilities in transitive dependencies that manual code review would miss.
4. Treat CSS from Untrusted Sources as Untrusted Input
If your application processes CSS files from external sources (user uploads, third-party fetches, CI builds of external repos), treat them with the same scrutiny as any other user input. Consider stripping or validating sourceMappingURL comments before passing CSS to any processor.
5. Relevant Standards
- CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
- OWASP: Path Traversal — covers detection, exploitation, and mitigation patterns
- OWASP Top 10 A01:2021 – Broken Access Control (path traversal falls under unauthorized file access)
Key Takeaways
sourceMappingURLvalues are attacker-controlled data if the CSS being processed originates from any external source — they must be validated as paths, not trusted as safe strings.- PostCSS 8.5.15 and earlier will read arbitrary
.mapfiles if given a craftedsourceMappingURLwith../sequences; upgrading to 8.5.18 closes this path. - Transitive dependency vulnerabilities require explicit overrides in pnpm — a lock file entry for
postcss@8.5.15persists until you force the resolution withpnpm.overrides. - "Not confirmed reachable" is not the same as "not exploitable" — the vulnerable code exists in the installed node_modules and could be activated by future project changes without a new security review.
- Path traversal primitives are valuable to automated exploit chaining tools — even without a direct exploit path today, removing them proactively reduces the attack surface against increasingly capable automated tooling.
How Orbis AppSec Detected This
- Source: The
sourceMappingURLcomment value embedded in a CSS file processed by PostCSS — content that can be controlled by whoever supplies the CSS input. - Sink: PostCSS's internal source map auto-loading logic in versions ≤8.5.15, which called Node.js
fs.readFileSync()(or equivalent) using the unsanitizedsourceMappingURLpath value resolved relative to the CSS file's directory. - Missing control: No path containment check — PostCSS did not verify that the resolved absolute path of the
.mapfile remained within the CSS file's directory before performing the read operation. - CWE: CWE-22 — Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
- Fix: Upgraded PostCSS from 8.5.15 to 8.5.18 via a
pnpm.overridesentry inpackage.json, ensuring all transitive dependents receive the version that validatessourceMappingURLpaths before file I/O.
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 security vulnerabilities in build tooling are just as consequential as those in runtime application code. PostCSS sits at the heart of nearly every modern JavaScript frontend build pipeline, and its source map auto-loading feature — a convenience for developers — became a path traversal vector because one input (the sourceMappingURL value) was not validated before being used to construct a filesystem path.
The fix is surgical and backward-compatible: PostCSS 8.5.18 adds a path containment check that rejects traversal sequences while leaving all legitimate source map references working as expected. Combined with a pnpm override to ensure every package in the dependency tree gets the patched version, this upgrade closes the vulnerability across all three affected Vue CLI plugins in a single, auditable change.
When you encounter similar patterns — any place where a string from an external source is used to construct a file path — apply the same principle: resolve to an absolute path, assert it starts with the expected base directory, and reject anything that doesn't. That single check is the difference between a useful feature and a path traversal vulnerability.