Skip to content

Update npm package postcss to v8.5.18 [SECURITY] - #9109

Open
hash-worker[bot] wants to merge 1 commit into
mainfrom
deps/js/npm-postcss-vulnerability
Open

Update npm package postcss to v8.5.18 [SECURITY]#9109
hash-worker[bot] wants to merge 1 commit into
mainfrom
deps/js/npm-postcss-vulnerability

Conversation

@hash-worker

@hash-worker hash-worker Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

This PR contains the following updates:

Package Change Age Confidence
postcss (source) 8.5.108.5.18 age confidence

PostCSS: 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 PreviousMap parses the /*# sourceMappingURL=PATH */ comment from any CSS string passed to process() and dereferences PATH against 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 resulting JSON.parse SyntaxError message. 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 — no from, no map, 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.js and is wired into every Input construction at lib/input.js:70-77.

Input constructor (lib/input.js:70-77):

if (pathAvailable && sourceMapAvailable) {
  let map = new PreviousMap(this.css, opts)
  if (map.text) {
    this.map = map
    let file = map.consumer().file
    if (!this.file && file) this.file = this.mapResolve(file)
  }
}

PreviousMap constructor (lib/previous-map.js:17-29):

constructor(css, opts) {
  if (opts.map === false) return
  this.loadAnnotation(css)
  this.inline = this.startWith(this.annotation, 'data:')

  let prev = opts.map ? opts.map.prev : undefined
  let text = this.loadMap(opts.from, prev)
  ...
}

Note opts.map === false is 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:

loadAnnotation(css) {
  let comments = css.match(/\/\*\s*# sourceMappingURL=/g)
  if (!comments) return
  let start = css.lastIndexOf(comments.pop())
  let end = css.indexOf('*/', start)
  if (start > -1 && end > -1) {
    this.annotation = this.getAnnotationURL(css.substring(start, end))
  }
}

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) — when prev is absent and the annotation is not an inline data: URI:

} else if (this.annotation) {
  let map = this.annotation
  if (file) map = join(dirname(file), map)
  return this.loadFile(map)
}
  • If opts.from is unset, file is undefined and the raw attacker-supplied path (e.g. /etc/passwd) is used directly.
  • If opts.from is set, path.join(dirname(file), attackerPath) is used. path.join does not block .. segments, so ../../../../../etc/passwd resolves outside the intended directory.

loadFile (lib/previous-map.js:86-92) is the sink:

loadFile(path) {
  this.root = dirname(path)
  if (existsSync(path)) {
    this.mapFile = path
    return readFileSync(path, 'utf-8').toString().trim()
  }
}

The bytes are stored in this.text. Input immediately invokes map.consumer() (lib/input.js:74), which constructs a SourceMapConsumer (lib/previous-map.js:33). When the file is not valid source-map JSON (the common case), source-map-js calls JSON.parse, and V8's SyntaxError message embeds the first ~10 bytes of the file content:

Unexpected token 'r', "root:x:0:0"... is not valid JSON

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:

  • Attacker controls: CSS input passed to postcss().process(css, opts?).
  • Server resources: any file readable by the Node process — typically including app config, environment files, SSH keys, /etc/passwd, /proc/self/environ, etc.
  • No mitigations: there is no path validation, scheme allowlist, traversal check, or symlink check. The only relevant check (startWith(annotation, 'data:')) routes inline URIs to decodeInline; everything else hits loadFile.

Primitives obtained:

  • (a) Arbitrary file read — bytes loaded into Node memory.
  • (b) Information disclosure — first ~10 bytes leaked via JSON.parse SyntaxError message.
  • (c) File-existence oracle — non-existent paths return silently from loadFile (existsSync is false → returns undefined → no map text → no consumer call → no error). Existent non-JSON paths throw. Existent JSON paths succeed silently. Three distinguishable states.
  • (d) DoS primitive — directing the read at /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, no map):

$ node -e 'const p=require("postcss"); \
  try { p().process("a{color:red}\n/*# sourceMappingURL=/etc/passwd */"); } \
  catch(e){console.log(e.message)}'
Unexpected token 'r', "root:x:0:0"... is not valid JSON

The first 10 bytes of /etc/passwd (root:x:0:0) are leaked.

Vector 2 — Relative .. traversal with opts.from set (simulates a build pipeline that pins from to the source file):

$ node -e 'const p=require("postcss"); \
  p().process("a{color:red}\n/*# sourceMappingURL=../../../../../etc/passwd */", \
              {from:"/var/www/html/styles/main.css", map:{inline:false}}) \
   .catch(e=>console.log(e.message))'
Unexpected token 'r', "root:x:0:0"... is not valid JSON

path.join('/var/www/html/styles', '../../../../../etc/passwd') resolves to /etc/passwd.

Vector 3 — File-existence oracle:

##### Existing non-JSON file → throws (file confirmed to exist)
$ node -e 'require("postcss")().process("a{}\n/*# sourceMappingURL=/etc/passwd */")'
SyntaxError: Unexpected token 'r', "root:x:0:0"... is not valid JSON

##### Non-existent file → returns silently (file confirmed absent)
$ node -e 'r=require("postcss")().process("a{}\n/*# sourceMappingURL=/no/such/file */"); console.log("ok")'
ok

Vector 4 — Custom file-content leak:

$ printf 'API_KEY=sk-secret-12345\n' > /tmp/server-secret.env
$ node -e 'require("postcss")().process("a{}\n/*# sourceMappingURL=/tmp/server-secret.env */")' 2>&1 | head -1
SyntaxError: Unexpected token 'A', "API_KEY=sk"... is not valid JSON

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):

const fs = require('fs');
const orig = fs.readFileSync;
fs.readFileSync = function(p){
  if (typeof p==='string' && p.startsWith('/etc')) console.log('[FILE READ]:', p);
  return orig.apply(this, arguments);
};
require('postcss')().process('a{}\n/*# sourceMappingURL=/etc/hostname */');
// → [FILE READ]: /etc/hostname
// → SyntaxError: Unexpected token 'D', "Debian-tri"... is not valid JSON
Impact
  • Arbitrary file read of any file readable by the Node process from any CSS-processing context that accepts attacker-influenced CSS. PostCSS has hundreds of millions of weekly npm downloads and is the standard CSS processor for build tools (webpack 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.
  • Confidentiality leak of the first ~10 bytes of the targeted file via JSON.parse SyntaxError. This is enough to recover SSH-key headers, environment-variable prefixes (API_KEY=sk…), /etc/passwd records, the start of /proc/self/environ, and other high-value secrets, and to fingerprint the host (Debian-tri… from /etc/hostname).
  • File-existence oracle with three distinguishable response states (silent success, JSON.parse error, no-such-file silence), enabling reconnaissance of the host filesystem layout and confirmation of installed software, user accounts, and configuration files.
  • DoS by targeting /dev/zero, /proc/kcore, very large files, or named pipes — readFileSync is a synchronous, unbounded read.
  • Default-on: triggered with 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 loadFile accepts 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:

  1. Refuse traversal/absolute paths in loadMap (defence-in-depth):

    // lib/previous-map.js
    loadMap(file, prev) {
      if (prev === false) return false
      if (prev) { /* unchanged */ }
      else if (this.inline) {
        return this.decodeInline(this.annotation)
      } else if (this.annotation) {
        let annotation = this.annotation
        // Reject schemes (other than data:, handled above) and absolute paths.
        if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(annotation)) return
        if (require('path').isAbsolute(annotation)) return
        if (!file) return  // No base path → cannot safely resolve.
        const base = require('path').resolve(require('path').dirname(file))
        const resolved = require('path').resolve(base, annotation)
        // Refuse anything that escapes the base directory.
        if (resolved !== base && !resolved.startsWith(base + require('path').sep)) {
          return
        }
        return this.loadFile(resolved)
      }
    }
  2. Require explicit opt-in to follow on-disk source-map annotations: gate the loadFile(map) call in loadMap behind an option such as opts.map.annotation === true or opts.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 Score: 7.5 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

References

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.js
Line: 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 passes map: false, attempts to load that path from disk as a "previous source map." This happens on every postcss.parse() / postcss().process() call by default (opt-out, not opt-in).

loadMap() builds the candidate path via join(dirname(opts.from), annotation), where annotation is 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. If opts.from is 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 the join(dirname(file), map) logic has existed unchanged since PostCSS 8.0.0 (Feb 2020), any file ending in .map remains 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 generate result.map, even when the caller never set the map option. If the loaded map has a sourcesContent field (common for maps emitted by bundlers/transpilers), that content is merged into result.map and returned to the caller — disclosing the traversed-to file's content to whoever supplied the CSS.

Attack Scenario
  1. A service accepts user-submitted CSS and runs it through PostCSS to lint/format/transform it, e.g. postcss().process(userCss, { from: '/app/uploads/user123/input.css', to: '/app/uploads/user123/output.css' }) — idiomatic usage; map option untouched.
  2. Attacker submits CSS containing /*# sourceMappingURL=../../../../some/other/app/dist/bundle.js.map */ (or an absolute path if from is unset).
  3. PostCSS reads that .map file and folds its sourcesContent into result.map.
  4. The service does what most build pipelines do with a truthy 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).
  5. Attacker retrieves the emitted map and reads out the traversed file's content.
Impact

Disclosure of the contents of arbitrary .map files reachable via path traversal (or absolute path when from is unset) from the process's filesystem. Affects any application processing CSS it does not fully trust without explicitly passing map: false. No authentication or user interaction beyond submitting CSS text is required.

Vulnerable Code
loadFile(path, cssFile, trusted) {
  if (!trusted && !this.unsafeMap) {
    if (!/\.map$/i.test(path)) {
      return undefined
    }
  }
  this.root = dirname(path)
  if (existsSync(path)) {
    this.mapFile = path
    return readFileSync(path, 'utf-8').toString().trim()
  }
}

loadMap(file, prev) {
  ...
  } else if (this.annotation) {
    let map = this.annotation
    if (file) map = join(dirname(file), map)
    let unknown = this.loadFile(map, file, false)
    ...
  }
}
Recommended Fix

Constrain the resolved path to remain inside the CSS file's own directory instead of relying solely on a filename-extension check:

loadFile(path, cssFile, trusted) {
  if (!trusted && !this.unsafeMap) {
    if (!/\.map$/i.test(path)) {
      return undefined
    }
    if (!cssFile) return undefined
    let root = resolve(dirname(cssFile))
    let resolvedPath = resolve(root, path)
    if (resolvedPath !== root && !resolvedPath.startsWith(root + sep)) {
      return undefined
    }
  }
  this.root = dirname(path)
  if (existsSync(path)) {
    this.mapFile = path
    return readFileSync(path, 'utf-8').toString().trim()
  }
}

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" .map file placed two directories outside a simulated project directory was read via a crafted sourceMappingURL comment in otherwise-innocuous CSS, with its sourcesContent appearing verbatim in result.map.toString() — with no map option set by the caller. A second harness confirmed the simpler no-from case reads an absolute path directly. A third harness confirmed map: false is the only current workaround. The attached fix branch closes both vectors while keeping all 660 existing unit tests green.

Severity

  • CVSS Score: 7.5 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Release Notes

postcss/postcss (postcss)

v8.5.18

Compare Source

  • Restricted loading previous source maps file to the opts.from folder for security reasons (use unsafeMap: true to disable the check).

v8.5.17

Compare Source

  • Fixed Maximum call stack size exceeded error.
  • Fixed Prototype hijacking for postcss.fromJSON().
  • Fixed Input#origin() for unmapped end position (by @​chatman-media).

v8.5.16

Compare Source

v8.5.15

Compare Source

  • Fixed declaration parsing performance (by @​homanp).

v8.5.14

Compare Source

v8.5.13

Compare Source

  • Fixed postcss-scss commend regression.

v8.5.12

Compare Source

  • Fixed reading any file via user-generated CSS.
  • Added opts.unsafeMap to disable checks.

v8.5.11

Compare Source

  • Fixed nested brackets parsing performance (by @​offset).

Configuration

📅 Schedule: (UTC)

  • Branch creation
    • At any time (no schedule defined)
  • Automerge
    • "before 4am every weekday,every weekend"

🚦 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.


  • If you want to rebase/retry this PR, check this box

This PR has been generated by Mend Renovate.

@hash-worker
hash-worker Bot enabled auto-merge July 27, 2026 13:35
@hash-worker

hash-worker Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

⚠️ Artifact update problem

Renovate failed to update an artifact related to this branch. You probably do not want to merge this PR as-is.

♻ Renovate will retry this branch, including artifacts, only when one of the following happens:

  • any of the package files in this branch needs updating, or
  • the branch becomes conflicted, or
  • you click the rebase/retry checkbox if found above, or
  • you rename this PR's title to start with "rebase!" to trigger it manually

The artifact failure details are included below:

File name: yarn.lock
error This project's package.json defines "packageManager": "yarn@4.16.0". However the current global version of Yarn is 1.22.22.

Presence of the "packageManager" field indicates that the project is meant to be used with Corepack, a tool included by default with all official Node.js distributions starting from 16.9 and 14.19.
Corepack must currently be enabled by running corepack enable in your terminal. For more information, check out https://yarnpkg.com/corepack.

@cursor

cursor Bot commented Jul 27, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Single dependency pin with no app logic changes; patch release intended to harden PostCSS source-map loading.

Overview
Bumps the Yarn next/postcss resolution from 8.5.10 to 8.5.18, pinning the PostCSS version pulled in through Next.js across the monorepo.

This addresses high-severity issues around attacker-controlled sourceMappingURL comments that could trigger arbitrary filesystem reads and .map disclosure during CSS processing (CVE-2026-45623 / GHSA-6g55-p6wh-862q and GHSA-r28c-9q8g-f849). No application code changes—dependency resolution only.

Reviewed by Cursor Bugbot for commit 5dd42d0. Bugbot is set up for automated code reviews on this repo. Configure here.

@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hash Error Error Jul 27, 2026 3:41pm
hashdotdesign-tokens Error Error Jul 27, 2026 3:41pm
petrinaut Error Error Comment Jul 27, 2026 3:41pm

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread package.json
"jsondiffpatch": "0.7.2",
"lodash": "4.18.1",
"next/postcss": "8.5.10",
"next/postcss": "8.5.18",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5dd42d0. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/deps Relates to third-party dependencies (area)

Development

Successfully merging this pull request may close these issues.

1 participant