Most developers have a ritual with new projects. You clone the repo, change into the directory, type npm install, and watch the terminal fill up with download progress. It is automatic. Comfortable. So routine that you stop thinking about it entirely.
That comfort is exactly what attackers have been counting on.
This year has been brutal for the JavaScript ecosystem in ways that have not gotten enough attention outside of security circles. The Axios library, which sits in roughly 174,000 downstream packages and pulls in somewhere around 100 million weekly downloads, was compromised in March 2026 through stolen maintainer credentials. A malicious dependency called plain-crypto-js was injected into two versions, and it downloaded multi-stage payloads including a remote access trojan onto machines belonging to developers who had no idea anything had changed. CISA issued an advisory. Most people just ran npm update and moved on.
That was not an isolated incident. It was part of a pattern that has been accelerating for over a year.
The Shai-Hulud Problem
In September 2025, a campaign compromised 18 widely used packages including debug and chalk. If you have ever worked on a Node.js project you have used those packages. They are not optional utilities most teams choose to include. They are woven into the dependency trees of virtually everything. Combined weekly downloads at the time exceeded 2.6 billion. The attackers did not need to trick anyone into installing something new. They just had to poison something developers were already trusting implicitly.
The campaign that became known as Shai-Hulud took things further. What made it especially dangerous was its self-propagation mechanism. Once a developer installed an infected package, the malicious preinstall script would harvest that developer's npm tokens and GitHub credentials, then use those credentials to publish poisoned versions of every package that developer maintained. The worm spread not through a traditional exploit chain but through the trust relationships baked into the npm ecosystem itself. By the time multiple waves of the campaign had played out, it had touched 796 packages with a combined 132 million monthly downloads.
The June 2026 Miasma attack introduced a refinement called the Phantom Gyp technique. An attacker places a 157-byte binding.gyp file inside a compromised package. That file triggers npm's implicit node-gyp rebuild call, an automatic C/C++ compilation step that npm runs for any package containing that file. The critical detail is that this happens with no lifecycle script required and with no protection from --ignore-scripts, which was the flag developers had been told would protect them. The Miasma worm used this technique to compromise 57 packages in a second wave after the initial infection, all within a two-hour window, targeting Red Hat's official @redhat-cloud-services namespace.
These were not obscure packages from unknown maintainers. They were packages from organisations developers had every reason to trust.
Why the Architecture Makes This So Difficult
The underlying problem is structural. When you run npm install, the package manager hands execution rights to every package in your dependency tree. Not just the packages you chose to install directly. Every transitive dependency. Packages you have never read, maintained by people you have never heard of, several levels deep in a tree you probably have not audited. The average npm project pulls in 79 transitive dependencies.
Each of those packages can define preinstall, install, and postinstall lifecycle scripts. Those scripts run automatically and silently with the full permissions of the user running the install. On a developer workstation that might mean access to AWS credentials sitting in environment variables, SSH keys, GitHub tokens stored by the git credential manager, CI/CD secrets, and cloud provider configurations. On a build server the situation is often worse, because build servers tend to have elevated access to production infrastructure by design.
GitHub themselves described this as the single largest code-execution surface in the npm ecosystem. That framing is worth sitting with. Not the largest vulnerability. The largest code-execution surface. Meaning the attack does not require finding a bug. It requires getting code into a package, which is significantly easier.
Attackers have three main ways to do that. First is account compromise, where a maintainer's credentials get stolen and used to publish a malicious version of a legitimate package. Second is dependency confusion, where an attacker publishes a public package with the same name as an internal private package, exploiting the way some package managers resolve scoped dependencies. Third is typosquatting, where a malicious package is published under a name close enough to a popular one that a developer with a typo in their install command will pull it in instead.
In late May 2026, Microsoft Threat Intelligence documented a dependency confusion campaign where a threat actor published malicious packages under organisational scopes that mirrored real internal corporate namespaces. The packages spoofed internal enterprise infrastructure URLs in their package.json to appear legitimate. One maintainer account involved had been sitting dormant since April 2024, when it was registered under bug bounty research branding. Two years later the same account was pushing obfuscated credential-stealing payloads. The patience involved is not reassuring.
What Actually Happens When You Get Hit
The Shai-Hulud payload execution chain is worth understanding in detail because it illustrates how far beyond simple credential theft these attacks have evolved.
The infection starts during npm install, where the malicious preinstall hook executes a loader script without any user interaction. The payload unpacks through multiple decoding layers, using ROT-based obfuscation variants followed by AES-128-GCM decryption. It then downloads the Bun JavaScript runtime and detonates the final payload. Before continuing, the malware validates the execution environment and can restrict itself to CI/CD environments only, which means some developer machines may never see any obvious signs of infection while build pipelines are being quietly drained.
The self-propagation step is where it turns into a worm. The malware republishes packages owned by the compromised maintainer using forged provenance metadata, which means it spreads wearing the identity of a trusted publisher. In one campaign there was also a destructive tripwire: if the malware detected interaction with a planted decoy token, it triggered a command that wiped the victim's home directory.
The stolen credentials cascade in a specific way. Stolen npm tokens enable further package poisoning. Stolen GitHub tokens enable repository manipulation. Stolen AWS credentials enable cloud access. Each layer of compromise opens the next one, and by the time a security team detects unusual activity in one place, multiple systems are often already affected.
In 2025 alone, attackers published nearly 455,000 malicious npm packages. Over 99 percent of all open source malware now targets npm according to Sonatype's 2026 report. This is not a niche concern for enterprise security teams. It is the environment every JavaScript developer is working in every day.
The npm v12 Response
GitHub announced earlier this month that npm v12, shipping in July 2026, will flip three defaults that have been permissive since npm was created.
The first and most significant change is that install scripts will be blocked by default. Running npm install will no longer automatically execute preinstall, install, or postinstall scripts from dependencies unless they are explicitly allowed in your project. This includes native node-gyp builds triggered by binding.gyp files, which closes the Phantom Gyp vector directly. The second change blocks Git dependencies by default. The third blocks remote URL dependencies like HTTPS tarballs.
The mechanism for managing this is a new command called npm approve-scripts. You run it, it shows you which packages in your dependency tree have scripts that are not yet covered by your policy, and you approve the ones you have actually reviewed. Those approvals get committed to your package.json and checked into source control, creating a documented record of which packages your team has explicitly trusted to execute code at install time.
The security argument for this is clear. Only about 2 percent of npm packages actually need install scripts to function. The vast majority use them as a convenience shortcut for build steps that could be performed explicitly. For those 2 percent, adding them to an explicit allowlist is a one-time cost. For the other 98 percent, blocking execution by default removes the attack surface entirely.
The friction argument against it is also real. Paul McCarty, a vulnerability researcher who has been tracking npm supply chain attacks closely, put it plainly: when the choice is between a build that works and a build that is less prone to malware, the former will always win. Developers under deadline pressure who encounter blocked scripts are likely to approve everything with a blanket flag just to make the warnings go away, which defeats the purpose entirely. The tools exist to do this correctly, but tooling availability and tooling adoption are different things.
What You Should Actually Do Before July
If you are running the npm CLI today, you are relying entirely on publisher-side defences and assuming nothing slips through the registry. That assumption has been wrong repeatedly this year.
The most immediate thing you can do is upgrade to npm 11.16.0, which enables advisory mode for the v12 changes. In advisory mode, warnings surface but nothing breaks yet. That gives you visibility into which packages in your dependency tree have scripts running during install, without forcing you to fix everything before you understand the scope of the problem.
Run npm approve-scripts --allow-scripts-pending in read-only mode first. It lists every package whose scripts are not yet covered by your policy without changing anything. Go through that list and think carefully about which packages actually need to execute code at install time. Most of them do not. Approve the ones that do, pinned to the specific version you have reviewed, and commit the result.
Switch --ignore-scripts on for your CI pipelines now rather than waiting for v12. Anything that breaks when you do that is a package that was silently running code in your build environment, and you should know which packages those are.
Consider moving to pnpm or Yarn Berry if you have not already. Both have been shipping consumer-side security defaults for some time. pnpm v11, released in April 2026, unified its security configuration into a cleaner API and blocks lifecycle scripts by default unless you explicitly allowlist them. The npm CLI is the only major package manager that had not shipped these consumer-side protections until the v12 announcement. The alternatives were ahead of this problem.
For packages your team publishes, set up trusted publishing if you have not already. Trusted publishing ties a package release to a verified CI/CD workflow rather than to credentials that can be stolen from a developer's machine. It does not prevent every attack vector but it raises the bar significantly for account compromise scenarios.
Pin your dependency versions in CI. A package that was clean when you last audited it is not guaranteed to be clean after an update. Automated dependency updates through tools like Dependabot are useful for staying current with security patches, but they should go through a review step rather than merging automatically. The Axios attack left detectable signals in the registry metadata before anyone ran npm install, including a missing trusted publisher block and a missing gitHead field. Tools like Socket and StepSecurity surface these signals automatically. Without those tools, checking a package's metadata manually takes about thirty seconds per package and would have caught that attack before it ran.
The Deeper Shift
What is happening in the npm ecosystem right now is a forced reckoning with a trust model that was designed for a different era. When npm was built, the open source ecosystem operated on a foundation of good faith. Packages were published by developers who wanted to share useful tools. The idea that someone would weaponise that distribution mechanism at industrial scale was not a primary design consideration.
That era is over. The attacks in 2025 and 2026 are industrialised, coordinated, and in some cases state-linked. The Shai-Hulud campaigns showed self-replicating worm behaviour. The dependency confusion attacks showed patience and careful reconnaissance. The Bitwarden impersonation package that was part of one Shai-Hulud wave shows attackers studying which tools developers trust specifically to impersonate them.
npm v12 is a structural response to a structural problem. Blocking install scripts by default, requiring explicit allowlists, and closing the Phantom Gyp loophole are not complete solutions but they remove the path of least resistance that attackers have been using repeatedly. The era of implicit trust in package installation is ending whether developers are ready for it or not.
July is not far away. The question is whether you find out what is running in your dependency tree on your own terms, or whether npm v12 breaks your CI pipeline on the first day it ships and you figure it out under pressure.
The inventory is not that hard. The install is the thing you have been doing automatically for years without thinking about it. It is worth thinking about it now.
Top comments (0)