All writing

The Safest npm Package May Be the One You Do Not Install Today

The latest npm worm is a reminder that release age, lockfiles, deterministic builds, and install-time defenses all belong in a practical supply-chain security baseline.

A chained and locked npm package beside a laptop, with practical dependency-security guidance.

On August 4, attackers compromised the GitHub account of the maintainer behind keyv, a package used across a huge part of the JavaScript ecosystem.

They did not publish an obviously fake package with a suspicious name. They compromised trusted projects, inserted malicious files, and released new versions through legitimate publishing infrastructure.

The affected package family included keyv, flat-cache, file-entry-cache, cacheable-request, cache-manager, and several related libraries. According to Aikido’s ongoing investigation, the campaign eventually reached at least 434 packages across 1,381 compromised versions, representing more than two billion monthly installs.

The scope figures reflected the live investigation at the time of writing and may continue to change.

That scale sounds extraordinary, but the entry point was painfully ordinary:

A developer or CI pipeline installs a new dependency version.

The package’s preinstall script runs automatically.

The malicious code begins searching for credentials.

This was not just malware. It was a worm.

The payload added a preinstall command to the affected packages. During installation, it downloaded the Bun runtime and executed a heavily obfuscated JavaScript payload.

That payload searched infected environments for secrets, encrypted the collected data, and exfiltrated it through attacker-controlled GitHub repositories. More importantly, it also tried to locate npm and GitHub credentials that could be used to compromise additional maintainers and publish malicious versions of their packages.

This is what makes worm-style supply-chain attacks so dangerous.

A compromised package is no longer only the final payload. It becomes a delivery mechanism for compromising the next package, maintainer, repository, or build pipeline.

The npm registry then becomes the distribution network.

This pattern did not begin with the Keyv incident. The original Shai-Hulud campaign appeared in September 2025 and used compromised maintainer accounts, malicious installation scripts, credential theft, and automated package republishing. GitHub removed more than 500 compromised packages and introduced additional measures around authentication, token lifetime, and trusted publishing in response.

Since then, researchers have observed new Shai-Hulud variants, copycat campaigns, compromised CI/CD pipelines, malicious releases with valid provenance, and payloads targeting npm tokens, GitHub credentials, cloud accounts, SSH keys, Kubernetes secrets, database credentials, and deployment platforms.

This was not the first supply-chain attack.

It will not be the last.

We have optimized package management for speed

Modern dependency tooling is designed to make updates almost frictionless.

A package releases a new version. An automated bot opens a pull request. CI passes. The update gets merged.

In many teams, being on the latest version is treated as a sign of good engineering hygiene.

Usually, that is reasonable. Staying permanently behind creates its own security and maintenance problems. Known vulnerabilities need to be patched, unsupported versions need to be replaced, and dependency debt eventually becomes expensive.

But there is an important difference between being reasonably up to date and becoming an unpaid beta tester for every package published five minutes ago.

When a trusted maintainer account is compromised, the malicious release initially looks like any other update. It may come from the correct package. It may be signed. It may have valid provenance. Its repository checks may appear normal. Your tests may still pass because stealing environment variables does not necessarily break application functionality.

For the first few minutes or hours, security researchers, registry operators, maintainers, and automated detection systems are still investigating.

Installing immediately means entering the highest-risk part of that timeline.

Give new releases time to become observable

For ordinary dependency updates, I increasingly believe teams should use a minimum release-age policy.

Do not automatically consume a version immediately after publication.

Wait at least 24 hours.

For less urgent updates, waiting 48 or 72 hours may be even more appropriate.

This delay creates a small but useful observation window. During that time:

The pnpm security documentation makes the same underlying point: malicious versions are often removed within hours, but there is always a gap between publication and detection. A minimum release age helps prevent projects from automatically installing packages during that dangerous window.

A delay is not a complete security control. It cannot guarantee that a malicious package will be discovered within 24 hours. Sophisticated malware may remain dormant or target only specific environments.

But it changes the odds.

Security is often about turning an immediate, automatic action into a deliberate and observable one.

There is also an important exception: do not blindly delay an urgent patch for a vulnerability that is actively being exploited. Release-age policies should distinguish routine upgrades from emergency security fixes. The goal is not to remain outdated. The goal is to stop treating every newly published version as automatically trustworthy.

Commit your lockfile

A minimum release age protects future updates. A lockfile controls what your project installs today.

Your package.json may declare something like:

"some-package": "^4.2.0"

That does not necessarily describe the exact version installed across every machine and build. It defines a compatible range.

The lockfile records the resolved dependency graph, including exact versions of direct and transitive dependencies.

Without it, two developers can install the same project at different times and receive different dependency trees. A production build can also resolve a newer transitive version that was never tested on a developer’s machine.

From a security perspective, this is dangerous because a compromised release can enter your environment without anyone explicitly changing the application code.

A lockfile gives you:

Aikido’s lockfile guidance correctly describes the lockfile as a single source of truth for dependency resolution. Without one, uncontrolled updates can introduce newly compromised versions and make it harder to determine exactly which dependencies were present during an incident.

Commit the lockfile to version control.

Review changes to it.

Do not dismiss a 3,000-line lockfile diff as meaningless generated noise. That diff represents executable code entering your application or build environment.

Use npm ci in CI

Committing a lockfile is not enough if the build pipeline is allowed to resolve dependencies differently.

For automated builds, use:

npm ci

instead of:

npm install

npm ci installs from the lockfile and fails when the lockfile and package.json do not agree. This makes the dependency graph more deterministic and prevents a build from quietly introducing an unreviewed package version.

Unit 42 specifically recommends using package-lock.json together with npm ci to prevent hidden dependency updates during builds.

This matters because CI environments frequently contain exactly the credentials attackers want:

A developer laptop is valuable.

A privileged release pipeline is often far more valuable.

Installation scripts should not receive automatic trust

The recent attack executed through a preinstall script.

Earlier npm attacks have frequently used preinstall, install, or postinstall hooks because package managers historically allowed dependencies to execute code during installation.

That is an enormous amount of trust to grant code that was downloaded seconds earlier.

Where practical:

pnpm v10, for example, disables automatic dependency postinstall execution and allows teams to explicitly approve packages that require build scripts.

Not every team can disable installation scripts immediately. Native modules and build tooling sometimes depend on them. But they should be treated as privileged executable behavior, not harmless package metadata.

A lockfile tells you what you installed. It does not tell you whether it is safe.

Lockfiles, version pinning, delayed updates, and reproducible builds are important, but none of them can determine whether a package contains malware.

A perfectly locked malicious version is still malicious.

This is where package intelligence and dependency firewalls become useful.

I have been following Aikido Security’s research around these npm campaigns, and this is one of the areas where their approach makes practical sense to me.

Aikido’s Safe Chain works around package managers such as npm, pnpm, Yarn, npx, and pnpx. It checks packages during installation and can block known malware, typosquatting, dependency-confusion attacks, and compromised releases before their code executes. It can also be integrated into local development and CI pipelines without requiring teams to replace their package manager.

More importantly, Aikido has repeatedly been involved in detecting and investigating active npm compromises, including the August 4 Keyv campaign.

That distinction matters.

Traditional vulnerability scanning usually answers:

Does this dependency contain a publicly known vulnerability?

Supply-chain malware detection needs to answer a different question:

Was this previously legitimate package just taken over and weaponized?

A package may have no CVE, no known vulnerable function, and no suspicious history. The only dangerous part may be the version published twenty minutes ago.

That requires fast threat intelligence, behavioral analysis, package-diff analysis, and visibility into newly published artifacts.

No security product should become an excuse to ignore basic dependency hygiene. A tool cannot compensate for permanently exposed tokens, unrestricted CI permissions, ignored lockfile changes, or blindly merged updates.

But using a package-security layer alongside good engineering controls is far better than assuming npm audit covers the entire problem.

It does not.

My practical npm security baseline

For projects I care about, this is the minimum standard I would aim for:

  1. Commit the lockfile and treat changes to it as reviewable code.
  2. Use npm ci in CI and production builds.
  3. Introduce a minimum package-release age of at least 24 hours for routine updates.
  4. Avoid automatically merging newly published dependency versions.
  5. Review dependency updates separately from unrelated application changes.
  6. Restrict or explicitly allow installation scripts.
  7. Use short-lived and narrowly scoped credentials in development and CI.
  8. Prefer trusted publishing instead of reusable npm publishing tokens.
  9. Scan both direct and transitive dependencies for malicious packages, not only known CVEs.
  10. Use tooling such as Aikido Safe Chain to block compromised packages before installation.
  11. Keep an inventory of dependencies so affected versions can be identified quickly.
  12. Be prepared to rotate credentials, not just downgrade a package, after a confirmed compromise.

The 24-hour delay is especially easy to underestimate.

It costs almost nothing for routine upgrades, yet it prevents your build system from volunteering to be among the first environments to execute a newly released package.

Trust should have a time dimension

Open source software is one of the foundations of modern development. The answer is not to stop using dependencies or to assume maintainers are careless.

Maintainers are targets too.

Their accounts, personal access tokens, CI workflows, and publishing pipelines are valuable precisely because the ecosystem trusts them.

A package can be trustworthy for ten years and become dangerous in ten minutes.

That means trust cannot be based only on the package name, download count, maintainer reputation, GitHub stars, or whether the release came through an official pipeline.

We also need to ask:

The JavaScript ecosystem will continue moving quickly. Attackers know that, and they are designing campaigns around our expectation that updates should be instant, automatic, and invisible.

We do not need to stop updating.

We need to stop confusing speed with safety.

Sometimes the safest version of a package is not the latest one.

It is the latest one that has survived long enough for the rest of the ecosystem to look at it.

Sources and further reading