Back to Blog
high SEVERITY8 min read

How Path Traversal happens in PostCSS Source Map Auto-Loading and how to fix it

A high-severity path traversal vulnerability (GHSA-r28c-9q8g-f849) in PostCSS versions prior to 8.5.18 allowed attackers to abuse the `sourceMappingURL` comment auto-loading mechanism to read arbitrary `.map` files outside the intended directory. The fix upgrades PostCSS from 8.5.15 to 8.5.18 in `frontend/package-lock.json` and pins the version via an `overrides` block in `frontend/package.json`. This closes a file disclosure primitive that, while not independently exploitable in all configurati

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published August 26, 2026•Reviewed August 26, 2026

Answer Summary

GHSA-r28c-9q8g-f849 is a high-severity path traversal vulnerability (CWE-22) in PostCSS's previous source map auto-loading feature, where a crafted `sourceMappingURL` comment could trick PostCSS into reading `.map` files outside the intended directory. It affects PostCSS versions before 8.5.18 and is fixed by upgrading to 8.5.18 and adding an `overrides` pin in `package.json` to ensure no transitive dependency re-introduces an older version.

Vulnerability at a Glance

cweCWE-22
fixUpgrade PostCSS from 8.5.15 to 8.5.18 and pin the version with a package.json overrides block
riskArbitrary .map file disclosure from the server filesystem
languageJavaScript / Node.js
root causeInsufficient sanitization of the file path derived from `sourceMappingURL` comments before opening the referenced source map file
vulnerabilityPath Traversal in PostCSS sourceMappingURL Auto-Loading

Introduction

The frontend/package-lock.json file in this project locks down every Node.js dependency used by the frontend build pipeline — including PostCSS, the widely-used CSS transformation library. A routine dependency scan by Trivy flagged that the locked version of PostCSS, 8.5.15, contained a high-severity path traversal flaw tracked as GHSA-r28c-9q8g-f849. The vulnerable code path lives inside PostCSS's previous source map auto-loading feature: the logic that reads a sourceMappingURL comment from an existing CSS file and automatically loads the referenced .map file before applying further transformations.

When PostCSS encounters a line like:

/*# sourceMappingURL=../../../etc/passwd.map */

the vulnerable versions did not adequately sanitize the embedded path before attempting to open it on disk. That single oversight is enough to turn a CSS processing step into an arbitrary file-read primitive.


The Vulnerability Explained

What is sourceMappingURL Auto-Loading?

When PostCSS processes a CSS file that was previously compiled (e.g., by Sass or another PostCSS run), it can optionally read the existing source map to preserve accurate source positions. It does this by parsing the trailing comment:

/*# sourceMappingURL=styles.css.map */

PostCSS then constructs a filesystem path by joining the directory of the CSS file with the value of sourceMappingURL and opens that file. The vulnerability is in the path-construction step: versions before 8.5.18 did not strip or reject path traversal sequences (../, URL-encoded variants, etc.) before resolving the final path.

The Vulnerable Pattern

Conceptually, the vulnerable logic resembled:

// Simplified representation of the vulnerable behavior in postcss < 8.5.18
const mapPath = path.resolve(cssFileDir, sourceMappingURLValue);
const mapContent = fs.readFileSync(mapPath, 'utf8');  // ← no traversal check

If sourceMappingURLValue is ../../../../etc/app-secrets.map, then mapPath resolves to a location entirely outside the project directory. PostCSS would dutifully read that file and expose its contents through the source map data structure — available to any subsequent plugin or caller that inspects the parsed result.

Attack Scenario

Consider a build server or CI pipeline that:

  1. Accepts user-submitted CSS files as part of a theme-customization feature.
  2. Runs those files through a PostCSS pipeline to apply vendor prefixes or other transforms.
  3. Returns the processed CSS (or logs/exposes the PostCSS result object) back to the requester.

An attacker submits a CSS file containing:

.evil { color: red; }
/*# sourceMappingURL=../../../config/database.json.map */

PostCSS (≤ 8.5.15) resolves the path, reads config/database.json.map (or any other .map-suffixed file the process has read access to), and embeds its contents in the internal map property of the result. If the application forwards that data — in an error message, a debug endpoint, or a compiled asset — the attacker receives the file contents.

Even in scenarios where the output is not directly returned, this constitutes an exploit primitive: a reliable file-read gadget that automated exploit-chaining tools can combine with other weaknesses (e.g., a separate information-disclosure bug) to achieve meaningful impact.

Real-World Impact for This Application

The affected file is frontend/package-lock.json, meaning PostCSS is part of the frontend build toolchain (Vite, based on the vite devDependency in package.json). In a typical CI/CD setup:

  • The build server processes CSS files from the repository.
  • If an attacker can influence the CSS content (e.g., via a pull request, a compromised upstream package, or a misconfigured artifact pipeline), they could trigger the traversal during the build step.
  • Sensitive files readable by the build process — credentials, .env files, internal configs — become potential targets.

The Fix

What Changed

The fix touches two files:

1. frontend/package-lock.json — the direct version pin:

 "node_modules/postcss": {
-  "version": "8.5.15",
-  "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
-  "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
+  "version": "8.5.18",
+  "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.18.tgz",
+  "integrity": "sha512-xdB1oSLHbz1vRWgCDalrCqEFTWzFlhqFC5tIHLMOSUIjhm3XXQ1qrFy8S/ESr1JYRRXqM3c1QFiMZUJdUTqyMQ==",

This replaces the vulnerable 8.5.15 artifact (and its SHA-512 integrity hash) with the patched 8.5.18 artifact. The new integrity hash ensures npm verifies the exact bytes downloaded from the registry, preventing substitution attacks.

2. frontend/package.json — the overrides pin:

+  "overrides": {
+    "postcss": "8.5.18"
+  }

This is the critical second layer of defense. Without it, a transitive dependency that declares "postcss": "^8.5.0" could cause npm to resolve a version anywhere in the 8.5.x range — potentially re-introducing 8.5.15 if the lockfile is regenerated or if a dependency update pulls in a new resolution. The overrides block forces every package in the dependency tree to use exactly 8.5.18, regardless of what their individual package.json files request.

How the Fix Closes the Vulnerability

PostCSS 8.5.18 tightens the path validation inside its source map loading code. The patched version rejects or normalizes paths that contain traversal sequences before constructing the final filesystem path, ensuring that the resolved file always remains within the expected directory boundary. Valid, well-formed sourceMappingURL values (e.g., styles.css.map or maps/styles.css.map) continue to work without any behavioral change — only malicious or malformed traversal paths are rejected.


Key Takeaways

  • PostCSS's sourceMappingURL parser was the attack surface — not generic CSS processing. Any pipeline that auto-loads previous source maps from untrusted CSS input was exposed.
  • Upgrading the lockfile is not enough on its own — the overrides block in package.json is what prevents transitive dependencies from silently re-introducing the vulnerable 8.5.15 build.
  • Build-time dependencies carry real risk — postcss is a devDependency, but it runs on the build server with access to the project filesystem, making file disclosure a credible threat.
  • Integrity hashes matter — the new integrity field in package-lock.json cryptographically binds the resolved package to the exact patched artifact, preventing registry substitution.
  • Exploit primitives deserve proactive removal — even if the traversal is not directly exploitable in your current configuration, it is a reliable gadget that automated exploit-chaining tools can leverage alongside other weaknesses.

How Orbis AppSec Detected This

  • Source: The sourceMappingURL value embedded in a CSS file processed by PostCSS — externally controllable if user-supplied or third-party CSS is accepted.
  • Sink: PostCSS's internal source map auto-loading logic, which calls fs.readFileSync (or equivalent) on a path derived from the sourceMappingURL comment without adequate traversal sanitization — present in node_modules/postcss at version 8.5.15 as locked in frontend/package-lock.json.
  • Missing control: No path normalization or boundary check to ensure the resolved .map file path remained within the project or CSS file directory.
  • CWE: CWE-22 — Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').
  • Fix: PostCSS was upgraded from 8.5.15 to 8.5.18 in frontend/package-lock.json, and an overrides block was added to frontend/package.json to pin the version across all transitive dependencies.

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 sharp reminder that CSS processing is not a passive, read-only operation. PostCSS's source map auto-loading feature — a convenience for preserving accurate source positions across multi-step builds — became an arbitrary file-read primitive when path traversal sequences in sourceMappingURL comments were not sanitized. The fix is precise: upgrade to PostCSS 8.5.18, which tightens path validation, and lock that version in place with an overrides block so no transitive dependency can quietly pull the vulnerability back in.

For teams running PostCSS in their build pipelines — which is nearly every modern frontend project using Vite, webpack, or Create React App — this upgrade is straightforward and carries zero risk of breaking valid CSS workflows. The only inputs rejected by the patch are malformed, traversal-containing paths that should never have been accepted in the first place.

Audit your package-lock.json today, add SCA scanning to your CI pipeline, and treat your build toolchain with the same security rigor you apply to your production runtime.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #463

Related Articles

high

clean_path() Path Traversal: rel_path Escapes USERDATA Dir

A path traversal flaw in the `clean_path()` helper let a user-controlled `rel_path` value escape the intended USERDATA directory and reach arbitrary files on disk. The function joined path segments without checking the final result, so `../` sequences in route parameters or query strings could be used to read or write files outside the sandboxed storage area. The fix normalizes the path and verifies it still resolves inside the USERDATA root before returning it, raising an error otherwise.

critical

path.resolve() Path Traversal in Node CLI's Dynamic import()

A Node.js CLI script for validating expression definitions took a file path from `process.argv[2]`, resolved it with `path.resolve()`, and passed the result straight into a dynamic `import()` — with no check that the resolved path stayed inside the working directory. An attacker (or a malicious skill/plugin invocation) could supply traversal sequences to load and execute arbitrary `.js` files from anywhere on disk. The fix adds a boundary check with `path.relative()` and an extension allowlist b

high

bookDir() Path Traversal via Unsanitized bookId Parameter

The `bookDir()` function accepted unsanitized `bookId` values derived from user-created book titles, enabling path traversal attacks through `../` sequences. A fix was applied that validates the identifier using `path.basename()` and throws on mismatch, ensuring all resolved paths remain within `LIBRARY_DIR`.

high

Express `app.get('*')` Wildcard Handler Path Traversal in watch.js

A first-party Express server's wildcard route handler used `req.url.indexOf('font.woff2')` to gate access to a font file, allowing attackers to bypass the substring check with crafted paths. The fix replaces the catch-all handler with explicit route registration.

high

updateCardBg() Follows Unvalidated 302 Location Headers

A background-image updater fetched a configured image URL with manual redirect handling and then re-issued the request to whatever `Location` header came back, with no scheme or host checks. A redirect to `http://169.254.169.254/` or `http://127.0.0.1:<port>/` would have been followed with the original fetch options attached, and the response body written to disk as an image asset. The fix resolves the redirect target against `imgDownloadUrl` and rejects anything that is not HTTPS on the same ho

high

markitdown_bridge.py Path Traversal: Arbitrary File Read via sys.argv

The markitdown_bridge.py script, used by MDView for DOCX-to-Markdown conversion, accepted file paths directly from command-line arguments without validating they stayed within intended directories. An attacker could exploit this to read arbitrary files from the filesystem by passing path traversal sequences in the source_path parameter.