Research

  • NPM
  • Cybersecurity
  • Blockchain
  • Software Supply Chain
  • JavaScript

JavaScript Supply Chain Scare: When npm install Leads to Full Compromise

Installing a familiar SDK can quietly run a malicious preinstall script with access to your development environment.

Published byIda Swarczewskaja9 min read
JavaScript Supply Chain Scare: When npm install Leads to Full Compromise

Installing a familiar SDK can quietly run a malicious preinstall script with access to your development environment.

Somewhere inside that install step, a worm wakes up, grabs your npm token, your GitHub token, your AWS keys, and quietly uses your identity to infect hundreds of other packages.

This is the second wave of the Shai-Hulud worm.

Act I: The sandworm under your package.json

Security teams at Aikido had met Shai‑Hulud before. In September, they wrote up a campaign where a worm abused npm packages in the nx ecosystem. A few months later, their dashboards lit up again.

This time, the targets were bigger:

  • SDKs and packages from AsyncAPI, PostHog, Postman, Zapier, ENS Domains, Browserbase and others
  • ~492 packages in total
  • Around 132 million monthly downloads across those packages

On the surface, the attack looked simple: new versions published under trusted names. But the payload was anything but simple.

The infection chain

The second wave looks roughly like this:

  1. Steal a maintainer’s publishing power The attacker gets hold of real npm tokens for real maintainers. Compromised accounts push fresh versions of popular packages, signed exactly the way a normal release would be.
  2. Hide in a lifecycle script Those releases add a preinstall hook that pulls in setup_bun.js. That script ensures the Bun runtime is available, then runs a second script, bun_environment.js, where the worm logic lives. The key detail is that npm install can execute preinstall before your application imports or calls any package code.
  3. Hunt secrets, everywhere Once the Bun environment is live, the code starts rummaging:
  • Environment variables
  • Local cloud config files
  • Files and directories that usually hold credentials
  • CI‑related folders

It even calls out to TruffleHog to do deep secret scanning, then uploads whatever it finds into attacker‑controlled public GitHub repos named things like “Sha1‑Hulud: The Second Coming”. Wiz sampled around 20k of those repos and found hundreds of valid GitHub, AWS, GCP and Azure credentials even after removing duplicates.

  1. Turn your infra into an infection vector On GitHub, the malware registers the machine as a self‑hosted runner called SHA1HULUD. Then it drops workflows such as .github/workflows/discussion.yaml that can execute arbitrary code whenever someone opens a Discussion or runs a particular job. It also adds workflows that serialize all GitHub Actions secrets to a file, upload them as an artifact, and then quietly clean up the branch. In cloud environments it goes after IAM roles and secret managers, trying to turn any key it finds into more access.
  2. Destroy data if it can’t move sideways Aikido’s analysis mentions a nasty fallback: if the worm can’t talk to GitHub or npm, it may simply wipe the user’s home directory.

So the “side effect” of installing a tainted dependency ranges from “your npm token got stolen” to “your laptop is blank”.

Act II: When a real product gets bitten

All of that might still sound abstract until you realize real SDKs you’ve probably used were carrying this thing.

PostHog’s co‑founder publicly confirmed that several versions of their JavaScript and Node packages briefly contained the malicious code:

  • posthog-node
  • posthog-js
  • posthog-react-native
  • posthog-docusaurus

Their story is the uncomfortable one many teams recognize:

  • A publishing token was compromised outside their CI
  • Malicious versions went out under their name
  • They rotated keys, unpublished the bad versions, and re‑released from a clean pipeline

Nothing in their product roadmap prepared them to become a worm host. The only “bug” they had to fix was where their secrets lived.

If you maintain any npm package, it’s hard not to map this onto your own setup:

  • Where are your publish tokens stored?
  • Could someone bypass CI and publish directly?
  • Would you even notice an unexpected version going out under your scope?

Act III: The npm install habit

Once you look at Shai‑Hulud’s behavior, you end up staring at a deeper question:

Why does npm install have the power to run arbitrary code from strangers as part of dependency resolution?

npm’s lifecycle scripts have existed for years to build native modules and download platform-specific binaries. Over time, huge swaths of the ecosystem started relying on them. Pull in a package, run its postinstall, and magic happens.

Shai‑Hulud just treats that design as what it really is: a remote code execution pipeline glued directly to your developer machines and CI.

Engineers responding to the incident have been converging on a few ideas.

1. Stop letting fresh code sneak into CI

One thread of thinking is about time.

Most teams still do something like:

npm install

in CI, based on version ranges:

"dependencies": {
  "some-sdk": "^3.26.0"
}

If you happen to build during the hour a malicious 3.27.0 is live, you become part of the blast radius.

Mitigations people are rolling out:

  • Use npm ci or pnpm’s equivalent in CI so installs follow the lockfile exactly.
  • Pin dependencies to exact versions as much as possible; review each dependency update explicitly.

And then comes the interesting idea: dependency cooldowns.

Both pnpm and Bun now have features that let you declare “don’t install a version that was just published; only accept it after some minimum age.” Your production builds intentionally lag behind npm by a few days so someone else hits the landmine first.

Python folks note you can approximate the same pattern using uv with --exclude-newer and requiring binary wheels (--only-binary=:all:), which avoids executing setup.py during installation.

Shai-Hulud illustrates why teams may accept slower upgrades to limit exposure to newly compromised packages.

2. Stop running scripts you don’t need

The second idea is about what install is allowed to do.

Tools like pnpm 10 and Bun can:

  • Disable lifecycle scripts by default
  • Ask you to explicitly allow scripts for specific packages
  • Let you configure which environments (dev vs CI vs prod) are allowed to run them at all

Native modules, platform-specific cryptography, and some CLI setups still require scripts, each of which needs review:

Scripts are suspicious until explicitly allowed, not harmless until proven guilty.

Combine explicit script approval with sandboxed installs in Bubblewrap, containers, or ephemeral runners to limit installation to the required files, hash checks, and reviewed code.

Act IV: The worm that eats tokens

Shai-Hulud also targets credentials and identities.

Every credential it finds, including npm tokens, GitHub tokens, AWS keys, GCP service accounts, and Azure secrets, is another chance to:

  • Push more malicious packages
  • Add more backdoored workflows
  • Pivot into more infrastructure

So the story naturally shifts from package managers to key management.

Static tokens on laptops are dead

The pattern that keeps coming up is:

  • No publish tokens on developer laptops
  • Tokens scoped to a single package or small set of actions
  • Tokens restricted to run only from specific IPs or runners

Some teams go further and issue dynamic, short‑lived credentials from Vault/OpenBao or similar systems:

  • Each build or pod gets a lease and a fresh set of credentials
  • When the lease ends, the credentials are revoked
  • Dumping the env vars from one job doesn’t give the attacker a long‑term key

For a worm that scans entire file systems for juicy tokens, this turns “we’re doomed forever” into “we have a tight window to rotate and respond.”

Trusted publishing and OIDC

npm’s Trusted Publishing model moves in the same direction: a GitHub Actions workflow presents an OIDC identity (“I am workflow X in repo Y”), and npm issues a short‑lived credential for that run.

PostHog’s plan after the incident is a good example of the mindset shift:

  • Kill the compromised token
  • Move publishing to OIDC‑based workflows
  • Add alerts for new versions that don’t match expected pipelines

Short-lived credentials remove the “steal a key today, publish next month” path, while an attacker running arbitrary code inside your CI can still request a token during that run.

Act V: Who should read all this code?

The social question lurking in all of this: who is actually responsible for understanding dependency code?

Security folks have a harsh answer: if your pipeline takes arbitrary third‑party code and pushes it to production without anyone ever looking at it, your team has accepted the risk of running unreviewed code.

Developers push back: in attacks like Shai‑Hulud, the code runs during install, possibly on a developer machine before any review or test. You can’t realistically expect every engineer to reverse‑engineer every dependency’s lifecycle script before they run npm install.

Some more realistic patterns emerge:

  • Assign dependency review to a security team, platform team, or rotating “librarian” who decides whether to bring in a new library.
  • Use central registries and features like PyPI’s “report project as malware” so deep audits can benefit entire ecosystems.
  • Add light process: design docs briefly justifying new dependencies, plus automated checks (SCA, license, known malware indicators).

The world where “library = magic box you never think about” is gone. The new world looks more like “libraries are vendors, with contracts and due diligence, even if the vendor is a solo maintainer on GitHub.”

Act VI: What this looks like in blockchain

Now zoom into the blockchain frontend world, where these patterns get amplified.

Dapps often ship:

  • Heavy JS frameworks
  • Several blockchain client libraries (web3.js, ethers.js, viem, chain‑specific SDKs)
  • Extra tooling for wallets, hardware devices, analytics, on‑chain indexing, graph clients

The result is… chunky.

A few data points:

  • One developer measured their web3 trading app at 1.2 MB of JavaScript, with web3.js alone contributing ~540 KB (45%) of the total and a 12.3‑second time to interactive on 3G.
  • A comparison of web3.js and ethers.js puts web3.js at roughly 620 KB bundle size by itself.
  • The Play2Dev dapp frontend roadmap bluntly describes web3.js as “legacy” with “several MB bundle size” and recommends avoiding it for new projects.
  • On the Polkadot side, a very simple dapp built with @polkadot/api, just initializing the API and fetching a balance, weighs in at around 1 MB before compression, still quite large even after gzip.
  • A Blocknative case study shows the Ledger Ethereum package weighing about 1.2 MB, with careful dynamic import needed just to trim a single module’s share of a wallet provider bundle.

This is before you add:

  • UI frameworks
  • Routing
  • State management
  • Analytics
  • “Just one more” helper SDK

From a performance point of view, that hurts users. From a supply‑chain point of view, each extra library is another potential Shai‑Hulud host. Web3 frontends are exactly the kind of apps that:

  • Live in the browser
  • Have direct access to wallets and signing
  • Need to be trusted by users to show accurate balances and transactions

Yet many of them ship multiple megabytes of JavaScript pulled from deep dependency trees that few people have ever read carefully.

Act VII: A quieter way: small, boring, zero‑dep libraries

That’s why at OKcontract Labs, we’ve been taking a different route for a while: zero‑dependency libraries wherever we can get away with it.

Each dependency adds specific risks:

  • Fewer dependencies mean fewer attack surfaces and fewer lifecycle scripts.
  • Smaller, well‑scoped libraries are easier to audit once and keep on a short leash.
  • A 5 kB helper you wrote and understand tends to be safer than a 600 kB dependency that drags in 20 other packages.

When you work in blockchain, that matters twice:

  1. Users already live in an environment that’s fragile and expensive (gas, latency, wallet UX).
  2. A compromise can expose the entire treasury to theft.

A dependency can be the right choice when its value justifies the trust it requires, and Shai-Hulud shows why each package deserves that review.

If you’re building in this space, it might be time to ask:

  • Can we swap a giant web3 client for a slimmer one?
  • Can we push more logic into audited contracts and expose smaller, simpler client code?
  • Can we replace stacks of convenience libraries with a few well‑tested utilities we actually understand?

That’s the direction we’re pushing toward at OKcontract Labs: high‑quality, low‑dependency building blocks that you can drop into frontends without dragging half of npm in behind them.

Shai‑Hulud won’t be the last worm to crawl through the registry. But we can choose how much food we leave out for the next one.

Originally published on Medium.