Update npm package postcss to v8.5.18 [SECURITY] - #9109
Conversation
|
PR SummaryLow Risk Overview This addresses high-severity issues around attacker-controlled Reviewed by Cursor Bugbot for commit 5dd42d0. Bugbot is set up for automated code reviews on this repo. Configure here. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
8ef3b40 to
5dd42d0
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 5dd42d0. Configure here.
| "jsondiffpatch": "0.7.2", | ||
| "lodash": "4.18.1", | ||
| "next/postcss": "8.5.10", | ||
| "next/postcss": "8.5.18", |
There was a problem hiding this comment.
Lockfile missing postcss security bump
High Severity
resolutions now pins next/postcss to 8.5.18, but yarn.lock still resolves postcss@npm:8.5.10 and has no 8.5.18 entry. CI runs yarn install --immutable, so this either fails install or keeps the vulnerable PostCSS that this security PR meant to replace.
Reviewed by Cursor Bugbot for commit 5dd42d0. Configure here.


This PR contains the following updates:
8.5.10→8.5.18PostCSS: Arbitrary file read and information disclosure via attacker-controlled sourceMappingURL in CSS comments
CVE-2026-45623 / GHSA-6g55-p6wh-862q
More information
Details
Summary
PostCSS's
PreviousMapparses the/*# sourceMappingURL=PATH */comment from any CSS string passed toprocess()and dereferencesPATHagainst the local filesystem with no scheme, allowlist, or traversal check. An attacker who controls the CSS input can cause the host process to read any file readable by Node and leak the first ~10 bytes of its content through the resultingJSON.parseSyntaxErrormessage. The bug also yields a precise file-existence oracle and a controllable-read primitive that may be combined with large-file targets for DoS. The behaviour is triggered with PostCSS's default options — nofrom, nomap, no plugins required — and is therefore reachable from any pipeline that runs untrusted CSS through PostCSS (CMS themes, user-uploaded styles, browser-extension/userstyle processors, build pipelines for third-party packages, blog comment renderers, etc.).Details
The dangerous chain lives in
lib/previous-map.jsand is wired into everyInputconstruction atlib/input.js:70-77.Inputconstructor (lib/input.js:70-77):PreviousMapconstructor (lib/previous-map.js:17-29):Note
opts.map === falseis the only short-circuit. With default options (opts.map === undefined), the rest of the constructor — including the filesystem read — executes.loadAnnotation(lib/previous-map.js:72-84) extracts the URL without sanitisation:getAnnotationURL(lib/previous-map.js:59-61) only strips the/*# sourceMappingURL=prefix and trims whitespace — no scheme check, no path normalisation, no allowlist.loadMap(lib/previous-map.js:124-128) — whenprevis absent and the annotation is not an inlinedata:URI:opts.fromis unset,fileis undefined and the raw attacker-supplied path (e.g./etc/passwd) is used directly.opts.fromis set,path.join(dirname(file), attackerPath)is used.path.joindoes not block..segments, so../../../../../etc/passwdresolves outside the intended directory.loadFile(lib/previous-map.js:86-92) is the sink:The bytes are stored in
this.text.Inputimmediately invokesmap.consumer()(lib/input.js:74), which constructs aSourceMapConsumer(lib/previous-map.js:33). When the file is not valid source-map JSON (the common case),source-map-jscallsJSON.parse, and V8'sSyntaxErrormessage embeds the first ~10 bytes of the file content:This error is propagated back to the caller. Any application that surfaces PostCSS errors (logs, HTTP 500 responses, build-tool output, debug pages) discloses those bytes to the attacker.
Trust-boundary analysis:
postcss().process(css, opts?)./etc/passwd,/proc/self/environ, etc.startWith(annotation, 'data:')) routes inline URIs todecodeInline; everything else hitsloadFile.Primitives obtained:
JSON.parseSyntaxErrormessage.loadFile(existsSyncis false → returns undefined → no map text → no consumer call → no error). Existent non-JSON paths throw. Existent JSON paths succeed silently. Three distinguishable states./dev/zero, very large files, or device files can stall or crash the process.PoC
All commands executed against this repository's HEAD (postcss 8.5.10) on Node v22.12.0.
Vector 1 — Absolute path, default options (no
from, nomap):The first 10 bytes of
/etc/passwd(root:x:0:0) are leaked.Vector 2 — Relative
..traversal withopts.fromset (simulates a build pipeline that pinsfromto the source file):path.join('/var/www/html/styles', '../../../../../etc/passwd')resolves to/etc/passwd.Vector 3 — File-existence oracle:
Vector 4 — Custom file-content leak:
The first 10 bytes of
/tmp/server-secret.env(API_KEY=sk) are leaked — sufficient to confirm a token's presence and, in many cases, recover its prefix.Filesystem-call trace (proves the read happens with no opts at all):
Impact
postcss-loader, vite, parcel, Next.js, Gatsby, etc.) and for runtime CSS-handling libraries (CSS Modules tools, CSS minifiers, theme processors). Any pipeline that runs untrusted user CSS — CMS theme uploads, user-styled blog posts, browser-extension/userstyle services, multi-tenant build farms, third-party-package build pipelines — is exposed.JSON.parseSyntaxError. This is enough to recover SSH-key headers, environment-variable prefixes (API_KEY=sk…),/etc/passwdrecords, the start of/proc/self/environ, and other high-value secrets, and to fingerprint the host (Debian-tri…from/etc/hostname).JSON.parseerror, no-such-file silence), enabling reconnaissance of the host filesystem layout and confirmation of installed software, user accounts, and configuration files./dev/zero,/proc/kcore, very large files, or named pipes —readFileSyncis a synchronous, unbounded read.postcss().process(css)and no options. The only configuration that disables the bug is the explicit, undocumented-for-this-purpose{ map: false }.Recommended Fix
The root cause is that
loadFileaccepts any path the attacker supplies inside a CSS comment. The annotation is meant for tooling, not for production CSS processing of untrusted input. Two layered fixes:Refuse traversal/absolute paths in
loadMap(defence-in-depth):Require explicit opt-in to follow on-disk source-map annotations: gate the
loadFile(map)call inloadMapbehind an option such asopts.map.annotation === trueoropts.map.followAnnotation === true. Today, the only way to opt out is{ map: false }, which also disables in-memory previous-map handling. Inverting the default — only follow disk-resident annotations when explicitly asked — eliminates the entire attack surface for callers that pass untrusted CSS, while preserving build-tool use cases where the annotation is trusted.A user-facing changelog entry should warn that
postcss().process(untrustedCss)previously read attacker-controlled paths, and recommend auditing applications that surfaced PostCSS errors to end users.Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
PostCSS: Path Traversal in Previous Source Map Auto-Loading (sourceMappingURL) leads to Arbitrary .map File Disclosure
GHSA-r28c-9q8g-f849
More information
Details
Vulnerability Details
File:
lib/previous-map.jsLine: 87-98 (
loadFile), 129-144 (loadMap)Root Cause
PostCSS auto-detects a
/*# sourceMappingURL=... */comment inside the CSS text it is asked to parse and, unless the caller explicitly passesmap: false, attempts to load that path from disk as a "previous source map." This happens on everypostcss.parse()/postcss().process()call by default (opt-out, not opt-in).loadMap()builds the candidate path viajoin(dirname(opts.from), annotation), whereannotationis the raw, attacker-controlled string from the CSS comment.path.join()normalizes but does not sandbox..segments, so a../../../prefix walks the resolved path outside the intended directory. Ifopts.fromis not set at all, the annotation is used completely unmodified — an absolute path in the CSS comment is read verbatim.8.5.12 already fixed a strictly worse variant of this (any file, any extension, could be read) by requiring the resolved path to end in
.map(loadFile()). That fix did not address the traversal itself, only the target extension. Since thejoin(dirname(file), map)logic has existed unchanged since PostCSS 8.0.0 (Feb 2020), any file ending in.mapremains readable through this path in the current release (8.5.16).Once loaded,
MapGenerator.isMap()treats the mere presence of a loaded "previous map" as an implicit request to generateresult.map, even when the caller never set themapoption. If the loaded map has asourcesContentfield (common for maps emitted by bundlers/transpilers), that content is merged intoresult.mapand returned to the caller — disclosing the traversed-to file's content to whoever supplied the CSS.Attack Scenario
postcss().process(userCss, { from: '/app/uploads/user123/input.css', to: '/app/uploads/user123/output.css' })— idiomatic usage;mapoption untouched./*# sourceMappingURL=../../../../some/other/app/dist/bundle.js.map */(or an absolute path iffromis unset)..mapfile and folds itssourcesContentintoresult.map.result.map— writes it next to the CSS output or returns it via API (source maps are meant to be consumed by browser devtools, so this is commonly public/served).Impact
Disclosure of the contents of arbitrary
.mapfiles reachable via path traversal (or absolute path whenfromis unset) from the process's filesystem. Affects any application processing CSS it does not fully trust without explicitly passingmap: false. No authentication or user interaction beyond submitting CSS text is required.Vulnerable Code
Recommended Fix
Constrain the resolved path to remain inside the CSS file's own directory instead of relying solely on a filename-extension check:
I've implemented, tested (full existing test suite — 660/660 passing, plus new PoC-based regression checks for both the traversal and legitimate same-directory cases), and can share this fix on request or via a private fork if invited.
Verification
Dynamically confirmed on v8.5.16 (current npm release / repo HEAD) via a standalone Node.js harness against
lib/postcss.js: a "secret".mapfile placed two directories outside a simulated project directory was read via a craftedsourceMappingURLcomment in otherwise-innocuous CSS, with itssourcesContentappearing verbatim inresult.map.toString()— with nomapoption set by the caller. A second harness confirmed the simpler no-fromcase reads an absolute path directly. A third harness confirmedmap: falseis the only current workaround. The attached fix branch closes both vectors while keeping all 660 existing unit tests green.Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Release Notes
postcss/postcss (postcss)
v8.5.18Compare Source
opts.fromfolder for security reasons (useunsafeMap: trueto disable the check).v8.5.17Compare Source
Maximum call stack size exceedederror.postcss.fromJSON().Input#origin()for unmapped end position (by @chatman-media).v8.5.16Compare Source
Input#origin()position (by @mizdra).rawsafter rehydrating a JSON AST (by @sarathfrancis90).nodesof new node (by @MahinAnowar).offsetinpositionBy()(by @greymoth-jp).rangeBy()onindex: 0(by @sarathfrancis90).v8.5.15Compare Source
v8.5.14Compare Source
v8.5.13Compare Source
postcss-scsscommend regression.v8.5.12Compare Source
opts.unsafeMapto disable checks.v8.5.11Compare Source
Configuration
📅 Schedule: (UTC)
🚦 Automerge: Enabled.
♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 Ignore: Close this PR and you won't be reminded about this update again.
This PR has been generated by Mend Renovate.