GCVE Workshop - 22 September 2026 (14:00-18:00), Luxembourg Before The Vulnopticon Conference - Registration

BREW-PNPM@10-CVE-2026-82393 (GHSA-VQ4V-J7R6-JQ4M)

Vulnerability from osv_homebrew – Published: 2026-09-03 09:52 – Updated: 2026-09-17 17:38 – Source website
VLAI
Summary
pnpm: A tarball dependency's manifest `name` escapes node_modules → arbitrary file write/overwrite on install
Details

Summary

When resolving a package, pnpm uses the resolved manifest name as a raw path segment for the isolated-linker import target. A tarball dependency whose package.json name is a scoped path traversal (@x/../../…/<abs path>) is therefore extracted outside node_modules, to an attacker-chosen absolute path, and can overwrite existing files there. Attacker controls the destination, filenames, and contents → arbitrary file write → code execution (e.g. ~/.zshrc, .git/hooks/pre-commit, another package's code). Occurs during pnpm install even with --ignore-scripts (no lifecycle scripts run), defeating that safety.

Same class as the just-patched GHSA-hwx4 (transitive-dependency alias traversal) and GHSA-v23m (stage download manifest name/version traversal), in a sink their fixes did not cover: the isolated-linker import target keyed by the resolved name.

Root cause

  • The isolated-linker import target is built with a raw path.join(modules, <resolved name>) in installing/deps-resolver/src/resolvePeers.ts:706, installing/deps-resolver/src/index.ts:614, and deps/graph-builder/src/lockfileToDepGraph.ts:233without the safeJoinModulesDir guard used on the symlink/hoisted/bin paths (installing/deps-restorer/src/lockfileToHoistedDepGraph.ts:222). The store location is node_modules/.pnpm/<id>/node_modules/<name>, so a traversal <name> escapes.
  • The only resolve-time name gate (resolving/npm-resolver/src/pickPackage.ts:753) rejects only unscoped names containing /, so a scoped @x/../.. passes.

Steps to reproduce

Self-contained PoC (real pnpm@11.9.0; loopback tarball server; escape target is a throwaway temp dir):

npm i pnpm@11.9.0
# host a tarball whose package.json name = "@x/"+"../".repeat(25)+"<abs>/OUTSIDE"; victim depends on the http URL
pnpm install --ignore-scripts

Confirmed output (repro/poc.mjs, exit 0):

escape dir is outside the project        : true
new file implanted outside node_modules  : true
pre-existing file OVERWRITTEN            : true
*** CONFIRMED: a tarball dependency wrote & overwrote files OUTSIDE the project during `pnpm install --ignore-scripts` ***

Remediation

Route the isolated-linker import-target joins (resolvePeers.ts:706, deps-resolver/index.ts:614, lockfileToDepGraph.ts:233) through safeJoinModulesDir (as the hoisted linker already does), and/or enforce validate-npm-package-name on the resolved manifest name (close the scoped-name gap at pickPackage.ts:753) so the import target rejects a traversal name and re-asserts containment before any write.


{
  "affected": [
    {
      "ecosystem_specific": {
        "fix": "bump",
        "range_state": "fixed",
        "upstream_fixed_in": "10.34.5"
      },
      "package": {
        "ecosystem": "Homebrew",
        "name": "pnpm@10",
        "purl": "pkg:brew/pnpm%4010"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "10.33.2"
            },
            {
              "fixed": "10.34.5"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "database_specific": {
    "confidence": "high",
    "source": "matched",
    "strategy": "registry",
    "upstream_evidence": [
      {
        "ecosystem": "npm",
        "key": "pkg:npm/pnpm@10.34.5",
        "name": "pnpm",
        "strategy": "registry",
        "subject_version": "10.34.5"
      }
    ]
  },
  "details": "## Summary\nWhen resolving a package, pnpm uses the resolved **manifest `name`** as a raw path segment for the isolated-linker import target. A tarball dependency whose `package.json` `name` is a scoped path traversal (`@x/../../\u2026/\u003cabs path\u003e`) is therefore extracted **outside `node_modules`**, to an attacker-chosen absolute path, and can **overwrite existing files** there. Attacker controls the destination, filenames, and contents \u2192 arbitrary file write \u2192 **code execution** (e.g. `~/.zshrc`, `.git/hooks/pre-commit`, another package\u0027s code). Occurs during `pnpm install` **even with `--ignore-scripts`** (no lifecycle scripts run), defeating that safety.\n\nSame class as the just-patched **GHSA-hwx4** (transitive-dependency *alias* traversal) and **GHSA-v23m** (`stage download` manifest name/version traversal), in a sink their fixes did not cover: the isolated-linker import target keyed by the resolved **name**.\n\n## Root cause\n- The isolated-linker import target is built with a raw `path.join(modules, \u003cresolved name\u003e)` in `installing/deps-resolver/src/resolvePeers.ts:706`, `installing/deps-resolver/src/index.ts:614`, and `deps/graph-builder/src/lockfileToDepGraph.ts:233` \u2014 **without** the `safeJoinModulesDir` guard used on the symlink/hoisted/bin paths (`installing/deps-restorer/src/lockfileToHoistedDepGraph.ts:222`). The store location is `node_modules/.pnpm/\u003cid\u003e/node_modules/\u003cname\u003e`, so a traversal `\u003cname\u003e` escapes.\n- The only resolve-time name gate (`resolving/npm-resolver/src/pickPackage.ts:753`) rejects only *unscoped* names containing `/`, so a **scoped** `@x/../..` passes.\n\n## Steps to reproduce\nSelf-contained PoC (real `pnpm@11.9.0`; loopback tarball server; escape target is a throwaway temp dir):\n```\nnpm i pnpm@11.9.0\n# host a tarball whose package.json name = \"@x/\"+\"../\".repeat(25)+\"\u003cabs\u003e/OUTSIDE\"; victim depends on the http URL\npnpm install --ignore-scripts\n```\nConfirmed output (`repro/poc.mjs`, exit 0):\n```\nescape dir is outside the project        : true\nnew file implanted outside node_modules  : true\npre-existing file OVERWRITTEN            : true\n*** CONFIRMED: a tarball dependency wrote \u0026 overwrote files OUTSIDE the project during `pnpm install --ignore-scripts` ***\n```\n\n## Remediation\nRoute the isolated-linker import-target joins (`resolvePeers.ts:706`, `deps-resolver/index.ts:614`, `lockfileToDepGraph.ts:233`) through `safeJoinModulesDir` (as the hoisted linker already does), and/or enforce `validate-npm-package-name` on the resolved manifest name (close the scoped-name gap at `pickPackage.ts:753`) so the import target rejects a traversal name and re-asserts containment before any write.",
  "id": "BREW-pnpm@10-CVE-2026-82393",
  "modified": "2026-09-17T17:38:54Z",
  "published": "2026-09-03T09:52:47Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/pnpm/pnpm/security/advisories/GHSA-vq4v-j7r6-jq4m"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-82393"
    },
    {
      "type": "WEB",
      "url": "https://github.com/pnpm/pnpm/pull/12872"
    },
    {
      "type": "WEB",
      "url": "https://github.com/pnpm/pnpm/pull/12890"
    },
    {
      "type": "WEB",
      "url": "https://github.com/pnpm/pnpm/commit/51300fd41c5e4c8f47635108e373cc3d1f324fa7"
    },
    {
      "type": "WEB",
      "url": "https://github.com/pnpm/pnpm/commit/78e29fe5583a1e5d69ea05e414eff310f78d5ed9"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/pnpm/pnpm"
    },
    {
      "type": "WEB",
      "url": "https://github.com/pnpm/pnpm/releases/tag/v10.34.5"
    },
    {
      "type": "WEB",
      "url": "https://github.com/pnpm/pnpm/releases/tag/v11.11.0"
    }
  ],
  "schema_version": "1.7.3",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "pnpm: A tarball dependency\u0027s manifest `name` escapes node_modules \u2192 arbitrary file write/overwrite on install",
  "upstream": [
    "GHSA-vq4v-j7r6-jq4m",
    "CVE-2026-82393"
  ]
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…