pnpm vs. Shai-Hulud: Rethinking application dependency security

How can a simple npm install become an entry point for exfiltrating an organization's secrets, and how can you mitigate that risk without giving up the JavaScript ecosystem?

Every time you run npm install, installation scripts from downloaded packages may be executed automatically, giving access to a dependency graph that can contain thousands of transitive packages. It only takes a single compromised package to expose sensitive data.

pnpm helps reduce this attack surface by isolating dependencies, blocking installation scripts by default, and enforcing a maturity period before newly published packages can be installed.

In 2025, the Shai-Hulud worm compromised npm packages that collectively accounted for millions of weekly downloads. It exfiltrated secrets and automatically replicated itself into other packages. The incident highlighted a structural weakness of the JavaScript ecosystem: its implicit trust in more than 2.5 million packages.

This analysis explores the mechanisms exploited by Shai-Hulud and the safeguards provided by pnpm:

  • Dependency isolation
  • Installation script blocking
  • Package maturity delays

Shai-Hulud: Anatomy of a Supply Chain attack

A supply chain attack compromises a target not through its own source code, but through a third-party dependency. Malicious code is injected into a trusted tool or library and then automatically distributed to every project that depends on it.

The JavaScript ecosystem is particularly exposed: the npm registry contains more than 2.5 million packages, and modern projects routinely rely on hundreds of transitive dependencies.

Named after the giant sandworms of the Dune universe, Shai-Hulud is a self-replicating worm targeting the npm package registry.

Here's how it works:

  1. A phishing campaign impersonates the npm registry, convincing maintainers to update their multi-factor authentication settings and exposing their credentials.
  2. Malicious code is injected into affected repositories. The payload is triggered the next time a consumer project runs npm install.
  3. The malware scans the environment for secrets (tokens, environment variables, API keys), exfiltrates them to an attacker-controlled endpoint, and publishes them to a public GitHub repository created under the victim's account.
  4. If the victim maintains or contributes to other npm packages, the malware automatically spreads using the stolen credentials.

In November 2025, a more aggressive variant appeared. It leveraged the preinstall hook, allowing execution earlier in the installation process, and attempted to delete the victim's home directory if exfiltration failed.

Among the documented victims was @ctrl/tinycolor, a package with several million weekly downloads.

npm: Vulnerable by Design?

Phantom Dependencies: An Expanded Attack Surface

npm, the dominant package manager in the JavaScript ecosystem, made architectural choices that simplify developers' lives but also broaden the attack surface.

To avoid excessive duplication of packages, npm flattens the node_modules folder. Transitive dependencies are hoisted as high as possible in the dependency tree.

For example, in a project using express, which itself depends on path-to-regexp:

my_app/

└── node_modules/

    ├── express/

    ├── …

    └── path-to-regexp/

This structure allows a project to import a package that is not declared in its own package.json, as long as that package exists somewhere in the dependency tree. These are often called phantom dependencies.

const regexp = require('path-to-regexp');

As a result, the effective attack surface extends to the entire transitive dependency graph—not just directly declared dependencies.

In addition, npm rebuilds this dependency tree on every installation, creating performance issues that have been well documented for years.

Installation Scripts: An arbitrary code execution vector

npm also allows packages to execute arbitrary code during installation via the preinstall, install, and postinstall lifecycle hooks.

Originally intended for legitimate tasks such as compilation or configuration generation, these hooks run automatically, transparently, and with the permissions of the current user.

This is precisely the mechanism exploited by Shai-Hulud to deploy its payload.

Defenses remain limited across popular package managers:

  • npm offers the --ignore-scripts flag, but it disables all scripts, including legitimate ones.
  • Since version 11.10 (February 2026), npm includes a min-release-age option that enforces a minimum delay before a package can be installed. Adoption, however, will take time.
  • Starting with npm 12.0 (July 2026), preinstall and postinstall scripts are blocked by default. Yet widespread adoption will be gradual, and this change does not address the underlying node_modules structure.
  • Yarn Classic (v1) provides no protection against these issues.
  • Since v2, Yarn offers a more secure architecture and optional script blocking through enableScripts, but these protections are not enabled by default and adoption remains significantly lower than Yarn v1.

While package managers are finally moving in the right direction, most still trust the entire registry, and everything it can execute, by default. This makes it increasingly relevant to consider alternatives that integrate security from the outset.

pnpm: Security by design

An Isolated node_modules Structure

pnpm ("performant npm") addresses npm's weaknesses while continuing to use the same central registry.

It eliminates phantom dependencies by exposing only explicitly declared dependencies to each package, blocks installation scripts by default since v10 (January 2025), and allows teams to reject packages that are too new through the minimumReleaseAge feature—directly countering key supply chain attack techniques.

pnpm maintains a global content-addressable store where each file from every package version is stored only once.

When installing a project, pnpm builds a two-level structure:

  1. A virtual store located in node_modules/.pnpm/, containing both direct and transitive dependencies as links to the global store.
  2. At the root of node_modules, only direct dependencies, represented as symbolic links to the virtual store.
my_app/

└── node_modules/
    ├── express/               (symlink to the virtual store)
    └── .pnpm/
        └── [email protected]/
            └── node_modules/
                └── express/   (hard link to the global store)

a { text-decoration: none; color: #464feb; } tr th, tr td { border: 1px solid #e6e6e6; } tr th { background-color: #f5f5f5; }

This approach offers three major advantages:

  • Faster installations, especially when dependencies already exist in the global store.
  • Lower disk usage, as files are no longer duplicated across projects.
  • Complete elimination of phantom dependencies: attempts to import undeclared packages fail immediately.

Script Blocking and Package Maturity Controls

Since pnpm v10, installation scripts are blocked by default.

Packages that legitimately need installation scripts must be explicitly approved, making it clear which dependencies are allowed to execute code during installation.

The minimumReleaseAge option adds another layer of protection by preventing the installation of packages published too recently—a valuable safeguard against attacks that rely on rapid distribution before malicious packages are detected.

Additional benefits include:

  • Native monorepo support
  • pnpm patch, which allows temporary modifications to dependency code directly within a project—useful for applying security fixes while waiting for an official release

Package Manager Comparison

CriterianpmYarn v1Yarn v2+pnpm
Installation scripts blocked by default
(Until v12)
Phantom dependencies
(by default)
Dependency isolation
(by default)
Package maturity delay before installation
(v11.10+)

(v4.10+)

(v10.16+)

Migrating to pnpm: What's the cost?

Typical Migration Steps

For many projects, migrating from npm is almost transparent:

rm -rf node_modules package-lock.json

pnpm install

pnpm run build

pnpm run test

pnpm generates its own lock file, pnpm-lock.yaml, which should be committed instead of package-lock.json.

A few manual adjustments may be required:

  • Update hardcoded paths referencing node_modules.
  • Fix phantom dependencies by explicitly declaring missing packages in package.json.
  • Approve packages that genuinely require installation scripts.
  • Replace npm ci with pnpm install --frozen-lockfile in CI/CD pipelines.

On a large production Angular application at SQLI—with approximately 1,500 transitive dependencies and several hundred unit tests—the migration took around two days, including documentation and feedback. With experience and existing migration guides, this effort can be reduced significantly.

pnpm's limitations

pnpm is not a silver bullet.

Several attack vectors remain:

  • Approved installation scripts still run with the current user's permissions.
  • Malicious code executed at runtime is not filtered.
  • Typosquatting attacks (publishing malicious packages with names similar to popular ones) remain possible.

Adopting pnpm should therefore complement—not replace—standard security practices such as:

  • Regular dependency audits
  • Version pinning
  • Manual review of updates before integration

These measures improve security regardless of the package manager in use.

Shai-Hulud did not bypass a security mechanism, it exploited a structural weakness: the ecosystem's implicit trust in 2.5 million packages, inherited from an era when npm hosted only a few thousand.

pnpm does not eliminate every threat. It cannot prevent a maintainer's credentials from being stolen, nor stop a legitimate package from introducing a regression. What it does provide are structural protections that significantly reduce the attack surface while also delivering measurable performance improvements and a relatively low adoption cost.

For that reason, pnpm deserves serious consideration for new projects and should be evaluated for existing ones, especially those exposed to dependency-related risks, which today means virtually all of them.

Several major open-source projects have already made the switch, including Vue, Vite, and Material UI.

Learn More

  • pnpm – Official Documentation
  • npm – Official Documentation
  • CISA – Widespread Supply Chain Compromise Impacting the npm Ecosystem

Are your application dependencies an entry point for supply chain attacks?