Malware

Inside the fourth wave of the Shai-Hulud npm worm

(Adobe Stock)

COMMENTARY: On the morning of August 4, someone took over the GitHub account behind keyv, a caching library with roughly 127 million weekly downloads.

They pushed malicious code from a new Shai-Hulud npm campaign straight to the main branch and cut a release. The poisoned versions were published to npm, carrying valid provenance, signed by GitHub Actions. Within hours the worm had spread to more than 1,280 packages representing more than 2 billion monthly installs, and researchers at Aikido Security and Endor Labs were watching 50 to 100 new packages fall every few minutes.

[SC Media Perspectives columns are written by a trusted community of SC Media cybersecurity subject matter experts. Read more Perspectives here.]

Most of the coverage will fixate on that number. Two billion is a good number. It’s also the least useful part of this story.

In this case, it’s useful know that the provenance (formal documentation) worked perfectly, but the attack still won.

Provenance attests to the build, not the code

Build provenance proves where and how a package was compiled and published. It says nothing about whether the source that went in was trustworthy. The attacker committed through the project’s real repository and triggered its real workflow, so the attestation was accurate. It honestly certified a poisoned build.

That distinction sounds academic until we watch it fail at scale. After the first three waves of this worm, industry guidance converged on two controls: scan our dependencies, demand attestation. Both are worth doing. Neither answers the question a developer has at install time: whether this code was about to steal their credentials. We adopted a control that verifies custody and started reading it as though it verified intent.

The trust root is a person, not a pipeline

Signing infrastructure can only inherit the trustworthiness of whoever can trigger it. Compromise a maintainer’s account and the pipeline will faithfully sign whatever that account tells it to. The cryptography has not been broken. It’s the human session in front of that’s the issue.

Think of this as the structural fact of an ecosystem built on volunteer maintainers. A single library can sit beneath tens of thousands of builds, and the credential protecting it was often one person’s session token. No amount of downstream verification fixes an upstream takeover, because afterward every downstream signal reads clean.

The theft happens at install, in memory

Every infected package carried a preinstall hook and a file named setup.mjs, an obfuscated dropper that quietly pulls down the Bun runtime and uses it to execute a 728 KB payload called Math_Symbol.js. Almost nothing malicious sits on disk waiting for scanning. The stealer runs in memory and finishes its work before npm install does.

What it takes is comprehensive. Extractors go after npm and GitHub tokens, AWS credentials, Kubernetes secrets, HashiCorp Vault tokens, Stripe and Slack keys. A scanner sweeps roughly 200 filesystem patterns for SSH keys, Terraform state, Docker registry credentials, KeePass databases and VPN configuration. Everything was encrypted and pushed to a public GitHub repository whose description read “Shai-Hulud: Here We Go Again.”

The reason a single hijacked account became 1,280 packages: continuous integration. Build runners install transitive dependencies and they hold publishing tokens for service accounts. The payload harvests those tokens and republishes poisoned versions of that victim’s own packages. No human attacker was involved in the spread.

That’s why the counter kept climbing while people were still reading the first advisory. The worm was recruiting the automation we built for convenience.

What to actually do about it

Here are five steps security teams can take:

  • Disable install scripts by default in CI: Running installs with scripts off blocks this entire class of attack, and it’s a configuration change rather than a purchase.
  • Pin versions and use overrides: The affected caching libraries are usually transitive, so a direct dependency audit will miss them.
  • Rotate on the assumption of exposure rather than evidence of it: If a runner or a laptop ran an install during the window, treat its tokens as burned.
  • Stop treating attestation as a verdict: Provenance remains a useful input to a trust decision. It’s not the decision.
  • Put a control at the moment of execution: Scanning and signing both operate before code runs. Something has to guard the moment it runs anyway, because that’s where this payload reveals itself and where the credential actually leaves.The signature was honest. The code was not.

Three prior waves taught the ecosystem to verify harder. This one shows the limit of verification: a system that proves a package was authentic cannot tell us it’s safe, and an attacker who owns the account gets authenticity for free.

Here’s what to do: Verify what the team installs. Then assume they got it wrong and control what executes.

Brad LaPorte, chief marketing officer, Morphisec

SC Media Perspectives columns are written by a trusted community of SC Media cybersecurity subject matter experts. Each contribution has a goal of bringing a unique voice to important cybersecurity topics. Content strives to be of the highest quality, objective and non-commercial.

Brad LaPorte

Brad LaPorte is the Chief Marketing Officer at Morphisec.

Get daily email updates

SC Media's daily must-read of the most current and pressing daily news

By clicking the Subscribe button below, you agree to SC Media Terms of Use and Privacy Policy.

Related Terms

Adware

You can skip this ad in 5 seconds