DEV Community

Cover image for Your Next npm install Could Already Be Running Malware
Rajat
Rajat

Posted on

Your Next npm install Could Already Be Running Malware

Why installing packages the moment they drop is now a security incident waiting to happen


Here is a scenario that is no longer hypothetical.

A legitimate, widely trusted npm package publishes a new version. You run npm install — or your CI pipeline does automatically — and within seconds, before a single line of your application code executes, a postinstall script fires. It scans your filesystem for AWS credentials, GitHub tokens, npm publish tokens, and Kubernetes secrets. It ships them to a command-and-control server. Then it quietly finds every package you have publish access to on npm and injects the same payload into new versions, releasing them under your name.

You did not get phished. No one stole your password. Your two-factor authentication is still on. And you are now both a victim and, unknowingly, a propagation vector.

This is not a thought experiment. This is precisely what happened on May 11, 2026, when a threat group called TeamPCP compromised 42 packages in the @tanstack/* namespace, publishing 84 malicious versions in a six-minute window. @tanstack/react-router alone sees over 12.7 million weekly downloads. The malicious tarballs carried valid SLSA provenance — the cryptographic attestation that security tooling uses to verify packages came from a trusted source. The certificates were legitimate. They signed the attack.

By the end of that day, over 170 packages across npm and PyPI had been hit, including Mistral AI, UiPath, and the Bitwarden CLI.

If you have been thinking of dependency management as an engineering convenience problem, it is time to reframe it. It is a security problem. And one of the most effective mitigations is almost embarrassingly simple: stop installing packages the moment they are published.

Have you ever had a dependency update break something unexpected in production? Drop a comment below — I want to hear where this hit you.


Why the npm Ecosystem Became a Target

The numbers tell part of the story. The npm registry now hosts over 2.5 million packages. The average enterprise JavaScript application pulls in hundreds of transitive dependencies — packages your application does not import directly, but which exist somewhere in the resolved dependency tree. Each one is a potential entry point.

The attack surface expanded gradually, driven by several forces working together.

Maintainer burnout and abandoned packages. Many popular packages are maintained by one or two people in their spare time. When maintainers lose interest, they often hand off the package — or simply disappear. Attackers have learned to identify unmaintained high-download packages and either request maintainer access (as happened with event-stream in 2018) or simply purchase the maintainer's account credentials from data breach marketplaces. The package's download count remains, but the code is now under hostile control.

Transitive dependency explosion. When you install a framework, you are not installing one package. You are pulling in an entire dependency graph. Some of those are two or three hops away from the package you actually intended to use. Auditing what you install directly is tractable. Auditing the entire resolved tree — which can run into the hundreds of packages for a moderately sized project — is almost impossible to do manually.

Semver ranges as a silent update mechanism. Most package.json files specify dependencies with caret or tilde ranges: ^1.14.0 or ~1.14.0. This is intentional — it means patch and minor updates arrive automatically on your next install. It is also what allowed the Axios attack to propagate so effectively. Projects pinned to axios@^1.14.0 automatically resolved to the malicious 1.14.1 on their next npm install or CI run. No developer action required.

AI-generated low-quality packages. Since late 2024, the registry has seen a surge of packages generated by or with AI assistance, many of which have thin audit trails, no meaningful version history, and no active community watching for anomalies. Some are benign but poorly maintained. Others are typosquats designed to look like real packages.

Blind trust in the install-time convention. The npm ecosystem normalized a convention — postinstall scripts — that was designed for legitimate use cases like compiling native binaries, but which creates an execution surface that runs with full developer privileges the instant installation completes. This runs before your application, before your tests, and before most security tooling can inspect what actually ran.


What Actually Happens When You Run npm install

Most developers think of npm install as a file-copy operation. It is not. It is a code execution event.

When you run npm install, the package manager resolves your full dependency tree, downloads tarballs, unpacks them, and — critically — runs any lifecycle scripts defined in each package's package.json. The relevant hooks are:

{
  "scripts": {
    "preinstall": "runs before the package is installed",
    "install": "runs during installation",
    "postinstall": "runs after the package is installed",
    "prepare": "runs after install and before pack/publish"
  }
}
Enter fullscreen mode Exit fullscreen mode

These scripts run with the same permissions as the user or process that invoked npm install. On a developer machine, that is typically your user account — with access to ~/.ssh, ~/.aws, ~/.npmrc (which contains your npm publish token), your shell history, and any environment variables you have set. On a CI runner, that means the runner's cloud credentials, OIDC tokens, and secrets injected by your pipeline.

The Axios attack exploited this directly. The malicious versions introduced a new dependency, plain-crypto-js@4.2.1, that had no presence in Axios's actual runtime code. It existed solely to execute a postinstall script that downloaded a second-stage remote access trojan. Normal application behavior was unchanged — your HTTP requests kept working. The malware ran silently at install time, then persisted.

The TanStack attack pushed this further. The worm installed a persistence daemon (com.user.gh-token-monitor.plist on macOS, a systemd user service on Linux) that polled your GitHub token every 60 seconds. If you followed the security playbook and revoked your token in response to a suspected compromise, the daemon detected the revocation and executed rm -rf ~/ — wiping your entire home directory as a retaliatory action. The correct response was the trigger.

Transitive execution is the hidden danger. When a deep dependency runs a postinstall script, you typically have no visibility into it. Your npm install output will mention it if logging is verbose, but most CI pipelines and developer workflows do not watch those logs carefully. The script runs and finishes before you see the result.

Lockfiles help, but not unconditionally. A lockfile (package-lock.json, yarn.lock, pnpm-lock.yaml) pins exact resolved versions, which prevents automatic version drift. If your lockfile pins axios@1.14.0, a new malicious 1.14.1 will not be pulled in automatically. But lockfiles require discipline. Any operation that resolves or updates the tree — npm update, npm install <new-package>, or a Renovate/Dependabot PR — can introduce new versions. And the Axios attack is a reminder that even a version explicitly installed based on a semver range can resolve to something you did not intend.


The Incident Record: What Has Already Happened

These are not hypothetical scenarios. They are documented incidents with real damage.

event-stream (2018). The event-stream package had over 1.5 million weekly downloads when a bad actor convinced the original maintainer to hand over publish access, claiming they wanted to help maintain it. The new maintainer injected a dependency targeting the copay Bitcoin wallet application. The attack went undetected for two months. The lesson: maintainer handoff is a trust event, not a routine admin action.

eslint-scope (2018). The eslint-scope package — part of the ESLint toolchain — was compromised when an attacker gained access to a maintainer's npm account via credentials stolen in a separate data breach. A malicious version was published that attempted to steal npm tokens from ~/.npmrc files and send them to a remote server. The package had millions of weekly downloads. Detection came from community monitoring, not from npm itself.

eslint-config-prettier (July 2025). A maintainer was phished via a convincing email sent from a lookalike domain. The attacker published unauthorized versions containing a postinstall script that executed a trojan DLL on Windows machines. The package had over 31 million weekly downloads at the time of compromise. The attack used a deceptive domain designed to resemble npmjs.com to steal credentials.

ua-parser-js (multiple incidents). The user agent parsing library was compromised more than once, with attackers injecting cryptocurrency miners and credential stealers. Given its reach across thousands of projects that passively depend on it through transitives, each incident resulted in broad exposure before detection.

Shai-Hulud (September 2025). In one of the largest npm supply chain attacks on record, a trusted maintainer was phished via a spoofed npm support email. Eighteen widely used packages with a combined weekly download count exceeding 2.6 billion were compromised. The malicious code targeted cryptocurrency transactions. CISA issued a public advisory. The attack was contained through community detection and not npm's own systems.

Axios (March 31, 2026). Microsoft Threat Intelligence attributed this attack to Sapphire Sleet, a North Korean state actor. Malicious versions 1.14.1 and 0.30.4 were published. They added a fake dependency that ran a postinstall script downloading a remote access trojan targeting macOS, Windows, and Linux. Projects using axios@^1.14.0 or axios@^0.30.0 resolved to the malicious versions on their next install. The trojan connected to a Sapphire Sleet command-and-control server with no visible change to Axios's behavior.

TanStack / Mini Shai-Hulud (May 11, 2026). The most technically sophisticated attack documented to date. No credentials were stolen. The attackers chained three GitHub Actions vulnerabilities — a pull_request_target Pwn Request, cache poisoning across the fork/base trust boundary, and runtime OIDC token extraction from the runner process — to publish 84 malicious packages in six minutes, all carrying valid SLSA provenance. The worm self-propagated using the victim's own publish credentials. OpenAI confirmed two employee devices were compromised. The campaign extended to over 170 packages across npm and PyPI by end of day.

The pattern across these incidents is consistent. A trusted package, a brief window of malicious publication, automatic adoption via semver ranges or CI pipelines, and detection by the community — not by the registry.

Have you audited your team's dependency update workflow recently? What does your process look like when a new version drops? Tell me in the comments.


Why "Wait Before Installing" Is Not Paranoia

The community security researchers who discovered the TanStack attack detected it approximately 20 minutes after the first malicious publish. The Axios malicious versions were identified within hours. In most recent incidents, the window between publish and public disclosure has been measured in hours, not days.

This is the core insight: attackers benefit from immediate adoption. Defenders benefit from delay.

If an organization had a policy of not installing packages released within the last seven days — enforced at the package manager or registry level — the Axios attack would not have reached their developers or CI pipelines. The TanStack attack would not have reached them either. The malicious versions were live for roughly four hours before deprecation began. A seven-day gate would have caught them with room to spare.

OpenAI's post-incident statement made this explicit. They noted that the two impacted employee devices "did not have the updated configurations that would have prevented the download of the newly observed package containing malware" — specifically referencing minimumReleaseAge configurations that they had begun deploying after the Axios incident but had not yet fully rolled out.

The waiting strategy works because:

  • Most compromised packages are detected within 24–72 hours by security researchers, community monitoring tools like Socket.dev, or registry scanning
  • Automated adoption (Renovate/Dependabot PRs merging, CI pipelines running npm install) is the primary propagation mechanism — any gate on version freshness interrupts that propagation
  • Malicious versions are typically removed or deprecated quickly once discovered, but only help organizations that have not already installed them
  • A brief delay has minimal practical cost for most dependency updates, which are not time-critical

The argument against waiting — that you need the latest security patches immediately — is real but addressable. The solution is an exclusion list for packages where you have an active vulnerability and are installing a confirmed-clean remediation version. That is a deliberate, audited decision, not an automatic update.


How Every Major Package Manager Now Supports This

All four major JavaScript package managers shipped native support for minimum release age gates. They arrived within months of each other, which suggests the ecosystem recognized the same threat simultaneously.

| Manager | Config File            | Unit    | Default  |
|---------|------------------------|---------|----------|
| pnpm    | pnpm-workspace.yaml    | minutes | 1 day    |
| Yarn    | .yarnrc.yml            | string  | none     |
| Bun     | bunfig.toml            | seconds | none     |
| npm     | .npmrc                 | days    | none     |
Enter fullscreen mode Exit fullscreen mode

pnpm was the first to ship, introducing minimumReleaseAge in v10.16 (September 2025). As of pnpm 11, it defaults to one day — the only manager that enables this out of the box.

# pnpm-workspace.yaml
minimumReleaseAge: 10080   # 7 days in minutes

minimumReleaseAgeExclude:
  - "@yourorg/*"
  - "some-critical-patch-package"
Enter fullscreen mode Exit fullscreen mode

Yarn shipped npmMinimalAgeGate in v4.10.0, supporting both raw minutes and human-readable duration strings.

# .yarnrc.yml
npmMinimalAgeGate: "7d"

npmPreapprovedPackages:
  - "@yourorg/*"
  - "typescript"
Enter fullscreen mode Exit fullscreen mode

Bun added minimumReleaseAge in v1.3. The value is in seconds. Bun's own repository uses a three-day gate.

# bunfig.toml
[install]
minimumReleaseAge = 604800   # 7 days in seconds

minimumReleaseAgeExcludes = ["@types/bun", "typescript"]
Enter fullscreen mode Exit fullscreen mode

npm shipped min-release-age in v11.10.0 (February 2026). The value is in days and has no exclusion mechanism yet.

# .npmrc
min-release-age=7
Enter fullscreen mode Exit fullscreen mode

Setting one of these takes under a minute. The protection is real and meaningful.


npm vs Yarn vs pnpm: How Lifecycle Scripts Behave Differently

The three managers handle install-time script execution differently, and those differences matter for security posture.

npm runs lifecycle scripts by default for all packages. You can disable them globally with --ignore-scripts, but this is a blunt instrument — it also prevents legitimate native build steps from running. npm has no per-package script opt-in mechanism at the lockfile level.

# Disable all scripts globally (blunt, often breaks native packages)
npm install --ignore-scripts

# Check for postinstall hooks before installing
npm pack <package> --dry-run
Enter fullscreen mode Exit fullscreen mode

pnpm gives you finer control. You can opt into scripts per package and audit which packages are requesting execution rights:

# pnpm-workspace.yaml
onlyBuiltDependencies:
  - esbuild
  - sharp
  - node-gyp
  - "@parcel/watcher"
Enter fullscreen mode Exit fullscreen mode

With this configuration, only the named packages are allowed to run lifecycle scripts. Everything else installs silently. This is arguably the most important security configuration in pnpm and worth adopting regardless of your minimum age policy.

Yarn supports enableScripts: false in .yarnrc.yml, disabling all install-time scripts globally. It also supports unsafeHttpWhitelist and other registry controls that give enterprises granular policy control.

pnpm's content-addressable store is worth understanding. pnpm stores packages in a central content-addressed store (typically ~/.pnpm-store) and creates hard links into node_modules. This means a given package version is physically stored once per machine, regardless of how many projects use it. It also means that if a malicious version is installed anywhere on the machine, its content is present in the global store — a nuance worth considering in shared CI environments.

Lockfile determinism. All three managers support frozen lockfile installs in CI, which is non-negotiable for security:

# npm
npm ci

# Yarn
yarn install --immutable

# pnpm
pnpm install --frozen-lockfile
Enter fullscreen mode Exit fullscreen mode

npm ci (or its equivalents) deletes node_modules and installs exactly the versions in the lockfile, failing if there is any discrepancy. This prevents resolution drift on CI even when developers update packages locally. If your CI uses npm install instead of npm ci, fix that first — it is a simpler and more immediate win than almost anything else in this article.


Practical Defensive Workflows

The following are concrete steps, not theoretical recommendations.

Pin exact versions for high-risk packages. For packages with broad filesystem access or network capabilities — package managers themselves, CLI tools, build tooling — prefer exact version pins over semver ranges:

{
  "dependencies": {
    "axios": "1.14.0",
    "lodash": "4.17.21"
  },
  "devDependencies": {
    "eslint": "9.26.0",
    "typescript": "5.8.3"
  }
}
Enter fullscreen mode Exit fullscreen mode

Exact pinning means updates require a deliberate change to package.json, not just a fresh install. It is friction by design.

Use npm ci in every CI pipeline — no exceptions:

# GitHub Actions example
- name: Install dependencies
  run: npm ci
  env:
    NODE_ENV: production
Enter fullscreen mode Exit fullscreen mode

Audit scripts before installing unfamiliar packages:

# Inspect the package tarball before installation
npm pack <package-name> --dry-run

# View lifecycle scripts defined in a package
npm info <package-name> scripts

# Check for postinstall specifically
cat node_modules/<package-name>/package.json | jq '.scripts'
Enter fullscreen mode Exit fullscreen mode

Set up minimum release age in your package manager config. The commands above give you the syntax. Set a seven-day gate for all projects not subject to emergency patching workflows. For monorepos with internal packages published to your own registry, use the exclusion list to bypass the gate.

Use Socket.dev for dependency scanning. Socket monitors npm packages in real time and alerts on suspicious patterns — new maintainers, unusual postinstall scripts, network calls in install hooks, and high-entropy strings that suggest obfuscation. It integrates with GitHub and has a free tier. The TanStack attack was detected in part through this kind of monitoring.

Configure Renovate with approval gates. If you use Renovate for automated dependency updates, require a human review step before merging dependency PRs for production packages:

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "packageRules": [
    {
      "matchPackagePatterns": ["*"],
      "automerge": false,
      "reviewersFromCodeOwners": true,
      "minimumReleaseAge": "7 days"
    },
    {
      "matchPackagePatterns": ["@yourorg/*"],
      "automerge": true,
      "minimumReleaseAge": "0 days"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Run dependencies in isolated environments. The TanStack attack postmortem recommended that developers not run npm install directly on their host OS but instead use Docker dev containers or VMs. If a postinstall script executes malware, the blast radius is the container — not your ~/.ssh directory.

# devcontainer.json (VSCode Dev Containers)
{
  "image": "mcr.microsoft.com/devcontainers/javascript-node:22",
  "postCreateCommand": "npm ci",
  "remoteUser": "node"
}
Enter fullscreen mode Exit fullscreen mode

Do not store secrets in .env files reachable by npm scripts. Use a proper secrets manager. The postinstall attack surface exists because secrets live in plaintext files that any process running as your user can read. If you use Doppler, Infisical, AWS SSO, or similar, secrets are not sitting on disk for a lifecycle script to find.


Enterprise Governance: Going Beyond the Developer Laptop

Individual practices matter, but organizations managing multiple teams and hundreds of repositories need structural approaches.

Private registries with quarantine policies. Tools like Artifactory, Verdaccio, and Cloudsmith allow you to mirror the npm registry through an internal proxy. You can configure this proxy to hold newly published versions in quarantine for a defined period before making them available to internal consumers. This enforces the minimum age policy at the infrastructure level, not just at the developer config level — meaning it applies to all teams uniformly, even those who have not updated their local configuration.

Developer -> Artifactory proxy -> [7-day quarantine buffer] -> npm registry
Enter fullscreen mode Exit fullscreen mode

SBOM generation and tracking. A Software Bill of Materials gives you an auditable record of every dependency in your application, including transitive dependencies, at the time of each build. SBOM generation tools like cyclonedx-npm or syft produce machine-readable inventories that can be diffed between builds to identify new packages entering the tree:

# Generate an SBOM for your npm project
npx @cyclonedx/cyclonedx-npm --output-file sbom.json

# Or using Syft
syft packages dir:. -o cyclonedx-json > sbom.json
Enter fullscreen mode Exit fullscreen mode

If a malicious package enters your dependency tree, an SBOM diff will surface it. Without SBOM tracking, you have no systematic way to answer "what changed in our dependencies between deploy A and deploy B?"

Dependency review boards. For large engineering organizations, consider a lightweight governance process for first-time package additions. Not for every update — that would create unsustainable overhead — but for net-new packages being added to a project. A brief async review asking "do we need this, is it maintained, what does its install script do" catches a meaningful portion of risk without slowing teams down.

Monitor your CI/CD environment as aggressively as your application runtime. The Axios and TanStack attacks both targeted CI runners, not production servers. If your security monitoring does not cover GitHub Actions runner behavior, npm lifecycle script execution in CI, and outbound network traffic from build pipelines, you have a significant blind spot.


The Mindset Shift That Matters

There is a comfortable assumption embedded in how most teams think about dependencies: that the packages we install are inert artifacts. Files. That npm install is like dragging a ZIP file into a folder.

That assumption is no longer safe.

Package installation is code execution. It happens with your credentials. It happens with access to your secrets. It happens in your CI environment, which has access to your production infrastructure. And it happens automatically, at scale, across every developer machine and pipeline run in your organization.

The good news is that most of the mitigations are not hard. npm ci instead of npm install in CI. A seven-line config change to enable minimum release age. A onlyBuiltDependencies list in pnpm. Socket.dev on your repositories. These are not expensive enterprise initiatives — they are configuration decisions that take an afternoon to roll out across a team.

The teams that got hit by the Axios attack in March 2026, and by TanStack in May 2026, were not careless. They used OIDC trusted publishing. They had two-factor authentication enabled. They ran SLSA provenance checks. Those controls protected against the attack classes they were designed for. They were not sufficient for the attack classes that arrived.

Waiting seven days before installing a newly published version costs almost nothing. It would have prevented both.

What is one change you are going to make to your dependency workflow after reading this? Tell me in the comments — I read every one.


Recap: What You Should Take Away

  • npm install executes code — via lifecycle hooks — with your permissions, before your application runs
  • Malicious versions are typically detected within hours of publication, not days. A seven-day install gate means you install after the detection window, not inside it
  • All four major package managers (npm, pnpm, Yarn, Bun) now support minimum release age configuration natively. It takes minutes to enable
  • Use npm ci (or equivalent frozen lockfile installs) in all CI pipelines. If you are not doing this, fix it today before everything else
  • pnpm's onlyBuiltDependencies is one of the most underused security controls in the ecosystem
  • Private registries with quarantine buffers enforce minimum-age policies organization-wide, not just per-developer
  • SBOM generation gives you a dependency audit trail — you cannot detect what changed if you never tracked what existed
  • The threat is not hypothetical. Axios, TanStack, Bitwarden CLI, Trivy, and over 170 packages hit in a single day in May 2026 are documented incidents with real organizational impact

Suggested Further Reading

  • TanStack Official Postmortem — tanstack.com/blog/npm-supply-chain-compromise-postmortem
  • Microsoft Threat Intelligence: Mitigating the Axios npm compromise — microsoft.com/security/blog
  • pnpm minimumReleaseAge documentation — pnpm.io/settings#minimumreleaseage
  • Yarn npmMinimalAgeGate documentation — yarnpkg.com/configuration/yarnrc
  • Socket.dev supply chain security — socket.dev
  • CISA Advisory on Shai-Hulud — cisa.gov/news-events/alerts/2025/09/23

Bonus Tips

Tip 1: Add audit=true to your .npmrc and npm audit --audit-level=high as a CI step. It will not catch zero-day malicious packages, but it catches known CVEs that have not been applied.

Tip 2: Review which packages in your tree are requesting install-time script execution. Run npm install --ignore-scripts once, then look at what breaks. Whatever breaks legitimately is your real onlyBuiltDependencies list.

Tip 3: If you use Renovate, set minimumReleaseAge in your Renovate config as well as your package manager config. Belt and suspenders — a version that has not passed the age gate should not even generate an update PR.

Tip 4: Check your CI runners for unexpected outbound connections. Most legitimate build steps do not need to reach arbitrary external endpoints. A network egress policy on your build environment is one of the few controls that would have limited the damage from the Axios attack even if the malicious version was already installed.


What Did You Think?

I want to hear from you directly. Have you already added a minimum release age gate to your projects? Are you using pnpm with onlyBuiltDependencies? Did this change how you think about npm install?

Drop a comment below — even a quick "already doing this" or "had no idea" helps me understand what the community is actually experiencing.


Found this useful? Share it with a teammate who still runs npm install without thinking twice. That is the audience that needs it most.


Want more articles like this on JavaScript security, platform engineering, and frontend architecture? Follow me here on Medium, or subscribe to my newsletter where I publish one practical insight every week.


Follow Me for More Angular & Frontend Goodness:

I regularly share hands-on tutorials, clean code tips, scalable frontend architecture, and real-world problem-solving guides.

  • 💼 LinkedIn — Let’s connect professionally
  • 🎥 Threads — Short-form frontend insights
  • 🐦 X (Twitter) — Developer banter + code snippets
  • 👥 BlueSky — Stay up to date on frontend trends
  • 🌟 GitHub Projects — Explore code in action
  • 🌐 Website — Everything in one place
  • 📚 Medium Blog — Long-form content and deep-dives
  • 💬 Dev Blog — Free Long-form content and deep-dives
  • ✉️ Substack — Weekly frontend stories & curated resources
  • 🧩 Portfolio — Projects, talks, and recognitions
  • ✍️ Hashnode — Developer blog posts & tech discussions
  • ✍️ Reddit — Developer blog posts & tech discussions

Top comments (0)