Skip to main content
SecurityAugust 4, 202611 min read

Eleven npm Packages Compromised in a 53 Minute Attack That Steals Every Credential Your Build Host Can Reach

On August 4, 2026 a worm pushed malicious versions of eleven npm caching packages inside a 53 minute window, harvesting npm tokens, GitHub PATs, AWS credentials, Kubernetes service account tokens and SSH keys. The headline was keyv and its 604 million monthly downloads, but keyv was the safest package on the list: its malicious release was a major version bump that no caret range accepts. The other ten were patch bumps, silently eligible for every dependency range in the ecosystem. That distinction, not the download count, decided who got hit. This is a practical guide to the defenses that actually change the outcome: what your semver range really grants, why npm install and npm ci are not interchangeable, when to disable install scripts and what breaks when you do, and how to check a tree you already have.

Most teams think of their dependency risk as a list of package names. It is not. It is a list of ranges, and a range is a standing grant of trust to a maintainer account you have never met, redeemable at any moment in the future without asking you again.

The npm attack of August 4, 2026 is an unusually clean demonstration of that, because it hit eleven packages at once and the outcome varied by range rather than by package.

What Happened

Between 09:35 and 10:28 UTC on August 4, 2026, malicious versions of eleven npm packages in the caching ecosystem were published. Security vendor Aikido, which published the first detailed analysis, attributes it to the Shai-Hulud worm family and reports that a maintainer's GitHub account was compromised, allowing malicious files to be committed directly to the main branch.

The mechanism was a preinstall script in package.json pointing at an obfuscated dropper, which meant the payload executed automatically during npm install. No import of the package was required. Installing was enough.

Per Aikido's analysis, the payload harvested npm tokens from ~/.npmrc, GitHub personal access tokens and OAuth and OIDC tokens, AWS credentials from files, environment variables and the EC2 instance metadata service, Kubernetes service account tokens, HashiCorp Vault tokens, Stripe and Slack tokens, SSH keys, .env files and Docker configs. It then exfiltrated them to public GitHub repositories, with a fallback domain and dynamic infrastructure resolved through an Ethereum smart contract. Aikido counts roughly 1,300 exfiltration repositories and reports onward spread to more than 400 additional packages.

That is the part every write-up covers. Here is the part that decides whether it touched you.

The Detail That Decides Everything

We checked all eleven packages directly against the npm registry rather than relying on the reporting. Every malicious version has since been removed, and latest on each package has rolled back to the release immediately below it. Comparing those two numbers is the whole lesson:

PackageLast goodMaliciousBump
keyv5.6.06.0.0major
flat-cache6.1.236.1.24patch
file-entry-cache11.1.511.1.6patch
cacheable-request13.0.1913.0.20patch
cacheable2.5.02.5.1patch
cache-manager7.2.97.2.10patch
ecto5.0.05.0.1patch
@cacheable/memory2.2.02.2.1patch
@cacheable/node-cache3.1.13.1.2patch
@cacheable/utils2.5.02.5.1patch
@cacheable/net2.1.02.1.1patch

Ten patch bumps and one major.

The major is keyv, the package every headline led with because of its download count. And keyv is precisely the one that could not spread through normal dependency resolution, because nothing depending on ^5.6.0 will ever resolve to 6.0.0. Semver forbids it. To be hit through keyv you had to explicitly ask for version 6 during that window.

The ten patch bumps are the opposite. A dependency on ^6.1.0 or even the conservative ~6.1.23 resolves to 6.1.24 without comment, without a changelog entry, and without any signal that anything unusual happened. Most of these are not packages you chose. flat-cache and file-entry-cache arrive underneath ESLint. They are three levels down in a tree nobody reads.

So the exposure ranking is inverted from the coverage. The famous package was structurally safe. The obscure transitive ones were the live vector.

What Your Range Actually Grants

A caret range is not a version. It is a policy: give me whatever this maintainer publishes next, as long as they label it compatible. The labelling is done by the publisher, and in a supply chain attack the publisher is the attacker.

This is worth stating plainly because the industry habit is to treat ^ as the safe default and pinning as paranoid. In terms of feature compatibility that is right. In terms of trust it is backwards. Every caret in your manifest is an open authorization that renews on every install, and the attacker's job is simply to publish inside it.

The practical consequence is that your attack surface is not the count of your dependencies. It is the count of distinct publisher accounts whose future releases your ranges will accept. Those are different numbers, and the second one is the one that matters.

The Lockfile Is the Control, and npm install Is Not npm ci

This is where most of the real-world outcome was decided, and it comes down to a distinction many teams have never had reason to examine.

npm ci installs exactly what package-lock.json says. It ignores your ranges entirely, and it fails loudly if the lockfile and the manifest disagree. During a 53 minute publication window, a project running npm ci in CI and in its Docker build was structurally incapable of pulling the malicious version, no matter how permissive its ranges were.

npm install is a different tool. It treats the lockfile as a starting point rather than a specification, and it will happily resolve a newer version within range and rewrite the lockfile to match. Run it during the window and it does exactly what the attacker needed.

So the rule is not "have a lockfile." Plenty of compromised projects had one. The rule is that every non-interactive install must be npm ci: CI pipelines, Docker builds, deploy scripts, container images, anything that runs without a human reading the diff afterwards. npm install belongs in exactly one place, which is a developer's terminal when they are deliberately changing dependencies and will review the resulting lockfile diff before committing it.

If your Dockerfile currently runs npm install, that is the single highest-value line to change in your entire supply chain posture. It costs nothing and it is the difference between "we were never eligible" and "we need to rotate every credential on the build host."

Turn Off Install Scripts, Then Find Out What Breaks

The payload here ran from a preinstall hook. That is the standard delivery mechanism, because it executes on install rather than on import, which means it reaches machines that never even use the package.

npm can refuse to run those:

npm ci --ignore-scripts

Or as a persistent setting for a project:

npm config set ignore-scripts true

The honest caveat is that this breaks packages which legitimately need a post-install step: native modules that compile or download a prebuilt binary, and tools that generate code. You cannot enable it blind and hope.

The useful move is to find out how small the problem actually is before assuming it is large. Most trees have far fewer script-running packages than people expect. On our own site the answer was exactly one, Prisma, which needs its client generated. Everything else in a thousand-package tree was inert.

Find yours:

find node_modules -maxdepth 3 -name package.json | xargs grep -l '"preinstall"\|"postinstall"\|"install"' 2>/dev/null

If that list is short, and it usually is, you can disable scripts globally and re-enable them for the named exceptions rather than leaving the door open for the entire tree. Note that npm v12 is moving toward blocking install scripts by default, which is the same conclusion arrived at from the other direction, and we covered what that change means for CI separately.

Verify Provenance, Not Just Integrity

The integrity hash in your lockfile proves the tarball has not changed since it was locked. It says nothing about whether the tarball was legitimate when it was published. A maliciously published version has a perfectly valid integrity hash.

npm's signature and attestation checks answer the different question of whether the registry's own signing chain agrees:

npm audit signatures

This is worth running in CI as a gate rather than as an occasional manual check. It will not catch everything, because a compromised maintainer account produces legitimately signed packages, but it does catch registry-level tampering and it costs one command.

How to Check a Tree You Already Have

If you want to know whether a specific project was exposed, the check is fast and does not require any tooling you do not have.

Start with the versions themselves, which is the definitive answer:

npm ls keyv flat-cache file-entry-cache cacheable-request cacheable cache-manager ecto

Compare against the table above. If every installed version sits below the malicious one, you were never eligible and you can stop.

If you want to be thorough, or you install with npm install somewhere and cannot be sure what a machine resolved, look for the payload rather than the version. Aikido published SHA-256 hashes for the dropper and its payload, and hashing is decisive where version numbers can lie:

find . -type f \( -name "setup.mjs" -o -name "Math_Symbol.js" -o -name "math_init.js" \) \
  -not -path "*/.git/*" | xargs shasum -a 256 2>/dev/null

The three hashes to match against are 54dc7ea5...b350668 and fd3ca400...5684b1eb for the dropper variants, and 9fc2570b...09cf1bcc for the payload. Be aware that setup.mjs is an ordinary filename and legitimate packages use it, so hash before you panic. In our own tree the only hits were a 525 byte gesture utility belonging to a motion library, which matched nothing.

Finally, the worm establishes persistence by committing to .claude/settings.json and .vscode/tasks.json, authored as "claude" with the message "chore: update config". Those are cheap to check and they cover the case where a developer machine was hit rather than a build:

git log --all --format='%an %s' | grep -i "chore: update config"

If You Find Something, the Cleanup Is Credential Rotation

Removing the package is not remediation. The payload's entire purpose was to read secrets off the machine and send them somewhere, and it succeeded or failed at that in the first seconds. By the time you find it, the package version is the least interesting artifact.

Anything the compromised machine could read is compromised. That means npm tokens, GitHub PATs and OAuth tokens, any AWS credentials reachable from that host including whatever the instance role could assume, Kubernetes service account tokens, SSH private keys, and the contents of every .env file on disk.

Rotate them in dependency order rather than alphabetically: the tokens that can mint other credentials first, because a stolen npm or GitHub token is what turns one compromised laptop into a compromised organization. Then audit for what was done with them during the window, which for GitHub means the audit log and for AWS means CloudTrail. A worm that self-replicates by republishing packages with stolen tokens has already told you what it does with them.

What Actually Changes the Outcome

Ranked by how much they move the needle, in our experience running other people's build infrastructure:

  1. npm ci everywhere non-interactive. One line in a Dockerfile. Structurally prevents the entire class.
  2. Commit the lockfile and review its diff like code. An unexplained version bump in a lockfile diff is a signal, and it is invisible if nobody reads it.
  3. Disable install scripts and allow-list the exceptions. Usually a shorter list than expected.
  4. npm audit signatures as a CI gate. Cheap, catches a real category.
  5. Know your credential blast radius from a build host. This is the one people skip, and it is what turns a contained incident into a long weekend.

Notice that none of these are "monitor for advisories faster." The publication window here was 53 minutes. No human process reacts inside that. The defenses that worked were the ones already in place before anyone knew there was anything to react to, which is the general shape of supply chain defense and the reason it is worth doing while nothing is on fire.

We build and maintain CI/CD pipelines with these controls as the default rather than as a hardening pass, and handle supply chain review as part of security and compliance work. If you are not sure whether your build hosts install with npm ci or npm install right now, that uncertainty is the finding. Get in touch.

Sources: Aikido analysis of the compromise, npm registry for version and removal status, npm ci documentation, npm audit signatures

Talk to the engineer who will own your stack.

No account managers, no offshore handoff. Senior DevOps, direct. Tell us what you are dealing with and you get a straight answer.