When npm install script malware turns up, the first suspect is a stolen npm token. This weekend's case points elsewhere: SafeDep found it "points to a compromised bot account or token, not a stolen npm token." A compromised release of @ethersphere/bee-js, the JavaScript client for the Swarm storage network, ran attacker code the moment it was installed, and it was published by the project's own release workflow with valid npm provenance. Every CI job, build server and coding agent that runs an install unattended runs that code with the machine's secrets. This article covers what happened, why the provenance badge did not help, and the check we would add so a new install script cannot run until a person has looked at it.
What happened
StepSecurity found that bee-js 13.3.0, "Published to npm on November 9, 2026, at 00:09:50 UTC, the release downloads and executes attacker-controlled JavaScript during installation." The payloads "target Swarm Desktop wallet files and attempt to install persistent access on Linux, macOS, and Windows."
The change that made it work was small. SafeDep's analysis shows two edits to package.json: the version script moved from prepare to postinstall, and the scripts folder was added to the files shipped in the package. In SafeDep's words: "With postinstall, the script runs on every install."
What it took is the part for wallet builders. The payload reads two files from the Swarm Desktop data folder. StepSecurity explains that "The swarm.key file is an encrypted Ethereum V3 keystore. The configuration can contain the password needed to decrypt it." SafeDep adds that 30 minutes later, the same code went into Swarm Desktop 0.56.2.
npm removed 13.3.0 at about 01:24 UTC, according to SafeDep, so it was live for roughly 75 minutes. During that window, StepSecurity noted, npm "still mapped latest to 13.3.0".
Why provenance did not help
Provenance answers one question: was this package built from this repository by this workflow? Here the answer was yes. StepSecurity: "The npm package therefore carries valid SLSA provenance linking it to the legitimate repository and workflow. The malicious code was already in the source that the workflow built."
The attacker used the project's release bot identity to create a commit, a tag and a GitHub release, and the release started the normal publish workflow. SafeDep found that the workflow "runs tests only for code pushed to a branch, so it ran none here", and that "Anyone who can create a release in this repository can publish code to npm with provenance."
Version ranges did the rest. StepSecurity lists Ethersphere's own tools depending on bee-js through ranges such as ^13.2.0 and ^13.0.0, and notes that "These ranges permit 13.3.0." Any fresh install or lockfile update in that window could pick it up.
Make install scripts opt-in
The attack needed one thing on your side: a package manager that runs a dependency's postinstall without asking. We would turn that off and keep a short list of packages that are allowed to run scripts. With npm, that means ignore-scripts=true in .npmrc, plus a CI check that fails when the lockfile adds a package with an install script nobody has reviewed. Here is a sketch of that check, reading the hasInstallScript flag npm writes into package-lock.json:
// Fail CI when a dependency with an install script appears
// that nobody has reviewed. Run it on every lockfile change.
import { readFileSync } from "node:fs";
type Entry = { version?: string; hasInstallScript?: boolean };
type Lock = { packages: Record<string, Entry> };
// Packages a person has read and allowed to run on install.
const ALLOWED = new Set(["esbuild", "sharp"]);
const raw = readFileSync("package-lock.json", "utf8");
const lock: Lock = JSON.parse(raw);
const flagged: string[] = [];
for (const [path, entry] of Object.entries(lock.packages)) {
if (!path || !entry.hasInstallScript) continue; // "" is the root
const name = path.split("node_modules/").pop() ?? path;
if (!ALLOWED.has(name)) flagged.push(`${name}@${entry.version}`);
}
if (flagged.length > 0) {
console.error("Install scripts need review:", flagged.join(", "));
process.exit(1);
}
What to notice: bee-js would have failed this check the day it gained a postinstall, even with valid provenance, because the check looks at what the package does on install and not at who built it. The allowlist is per package name, so a reviewed package that later changes its script still slips through. Pair it with a lockfile diff in review, and keep wallets, keystores and deploy keys off the machines that run installs.
If you maintain a package, the lesson from the other side is to put a person between a release and a publish. A GitHub environment with required reviewers on the publish job means a stolen bot token can still cut a release, but it cannot ship it to npm alone.
Where this costs you
Some packages need their install scripts: native modules and binary downloaders such as image or bundler tools. With scripts off, they break until you add them to the allowlist or run their build step yourself, and each one is a package someone has to read.
The check only sees install scripts. A malicious package that does its damage when your code imports it passes untouched, and so does a compromised version of a package already on the allowlist. It narrows one common path and leaves the others.
Pinning exact versions and installing from a lockfile stops a range from picking up a bad release mid-week, but it moves the risk to the day you update. Updating after a release has been public for a day or two, rather than within the hour, would have stepped around this one, at the cost of slower security fixes.
What to take from this
- If anything installed @ethersphere/bee-js 13.3.0 or Swarm Desktop 0.56.2, treat that machine as compromised and move any wallet it held.
- Read provenance as proof of where a package was built, not of what is in it.
- Turn off dependency install scripts by default and allow them per package after a review.
- Fail CI when the lockfile adds an install script nobody has reviewed.
- If you publish packages, require a person to approve the publish job.
If your team ships wallets, dApps or [Web3 products](/services/web3-blockchain) and wants a second pair of eyes on its build and release pipeline, book a conversation.
Sources
- StepSecurity, Ashish Kurmi, "@ethersphere/bee-js 13.3.0 Compromised", November 8, 2026: https://www.stepsecurity.io/blog/ethersphere-bee-js-13-3-0-compromise
- SafeDep, "Compromised bee-js and Swarm Desktop Steal Swarm Wallet Keys", November 9, 2026: https://safedep.io/blog/ethersphere-bee-js-npm-compromise



