A Web3 library shipped malware to npm with valid provenance. Block install scripts you never reviewed. A compromised release of @ethersphere/bee-js 13.3.0, the JavaScript client for the Swarm storage network, shipped attacker-controlled code that executed on install via a postinstall script while carrying valid SLSA provenance from the project's own release workflow. StepSecurity and SafeDep found the payload targeted Swarm Desktop wallet keystore files and attempted persistent access on Linux, macOS and Windows; npm removed the version after roughly 75 minutes live. The incident prompted a recommendation to set ignore-scripts=true and add a CI check that flags unreviewed install scripts in package-lock.json. Originally published on voirlabs.io https://www.voirlabs.io/insights/npm-install-script-malware-provenance?utm source=devto&utm medium=syndication&utm campaign=distribution by Asad Naeem. 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. 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". 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. 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