NPM hasn’t had a “quiet week” in a while has it?
Recent hits eslint-prettier, Nx, and chalk proved how quickly a compromised account can poison the ecosystem. Now here comes the “Shai-Hulud” campaign: hundreds of malicious packages, including @ctrl/tinycolor, pushed through stolen credentials. This campaign introduced a new twist: once a developer’s credentials were compromised, the malware attempted to spread automatically to other packages under their account.
The recommendations that follow aren’t new, but we’ve gathered them here as a single reference checklist you can point to when questions arise about how to reduce risk.
Why npm is a frequent target
npm has become a prime target for attackers because of its scale. With millions of developers depending on it daily, even a single compromised package can ripple through countless applications and organizations.
The NPM Ecosystem is characterized by significantly larger amounts of software reuse than other ecosystems and an incredibly dense maintainer ecosystem. According to “A study of The Security Threats in the npm ecosystem” by M Zimmerman et al. “One of the main characteristics of the npm ecosystem is the high number of transitive dependencies. For example, when using the core of the popular Spring web framework in Java, a developer transitively depends on ten other packages. In contrast, the Express.js web framework transitively depends on 47 other packages.”
Other notable findings of this study are:
Installing an average npm package introduces an implicit trust on 79 third-party packages and 39 maintainers, creating a surprisingly large attack surface.
Highly popular packages directly or indirectly influence many other packages (often more than 100,000) and are thus potential targets for injecting malware.
Some maintainers have an impact on hundreds of thousands of packages. As a result, a very small number of compromised maintainer accounts suffices to inject malware into the majority of all packages.
The influence of individual packages and maintainers has been continuously growing over the past few years, aggravating the risk of malware injection attacks.
Many of the recent npm compromises start the same way: an attacker gets control of a maintainer account usually through phishing, credential reuse, or a stolen token. Trusted Publishing via OIDC exists, but it’s optional. As long as long-lived tokens are common, attackers often don’t need to get fancy to push a malicious version.
Once inside, lifecycle scripts often do the heavy lifting. preinstall, postinstall, and similar hooks run automatically during npm install, which means attacker code executes by default on every developer machine and CI system that pulls the package. Unless you explicitly disable scripts, you’re betting your environment on every maintainer you transitively trust.
In theory, you could audit every version or rely on provenance statements, to check what’s “safe.” In practice, almost nobody in your supply chain does this at scale. The trust chain is too long and too fragile. Most organizations don’t have the time or budget to perform due diligence on hundreds of indirect dependencies, and attackers know it.
Recommendations for Security Teams
The most effective defenses against npm supply chain attacks come from reducing what an attacker can do if they manage to compromise a maintainer account or slip a malicious dependency into your environment. Security teams should focus on hardening build pipelines and controlling how new code enters and exits their environments.
Key steps include:
- Enforce integrity checks and pin versions in CI. Require lockfiles (package-lock.json, pnpm-lock.yaml) to be used in CI environments and teach developers to use npm ci rather than npm install unless they are adding, removing or updating a dependency.
- Opt out of `--ignore-scripts` rather than opting into them as a CI practice. The npm / yarn CLI has a flag called ignore-scripts that can be used to prevent the execution of any lifecycle hooks defined in the package.json file of the packages you are installing. This flag can be used with any npm command that installs packages, such as npm install, npm ci, npm update. Package managers like pnpm have started to disable the use of lifecycle scripts by default and have made them opt in. The practice of ignoring lifecycle scripts could potentially break existing codebases but educating developers on ignoring them by default and using them on an “as needed” basis will materially reduce the risk of them executing malware. You can obviously not patch human behavior but building this practice into existing security awareness campaigns adds value.
- Integrate malware detection. Run software composition analysis (SCA) and malware scanning in CI/CD and regularly on your codebase so that you can respond quickly when you are compromised. Supply chain risk is often implicitly accepted when software is reused. One can mitigate but never truly prevent it. Detective controls like malware detection help you effectively initiate incident response activities quickly. Making sure you have an up to date bill of materials and a quality malware feed is critical. Often, open source malware feeds are limited and the addition of proprietary feeds may be prudent.
- Delay package adoption for a period of time. While guidance varies across the industry, the Center for Internet Security Supply Chain Security Benchmark recommends organizations “ensure all packages used are more than 60 days old” for new software packages (not versions) that you use. When a new version comes out, you can use cooldown periods in automated dependency management tools, such as Dependabot allow you to reduce the probability that you update to a newly published and potentially malicious version of a software package. Setting a minimum release age in your pnpm settings can also assist here.
- Reduce the number of dependencies you use. This can primarily be done two ways. You can reduce software reuse for similar use cases. No you probably don’t need to use two pdf creation libraries in your application. Just use one. Everyone will thank you for the standardization except the supply chain hackers. You can also remove unused dependencies that are no longer where the code may not be used any longer due to application refactoring. You can use technologies like reachability analysis/call graph analysis to identify unused software dependencies in your packages.
These measures help contain organizational risk. But they only work if developers also adopt safe habits when managing dependencies and accounts.
Recommendations for Developers
If you’re writing code every day, you’re also on the front line of npm security. A few simple habits go a long way toward keeping malicious code out of your project:
- Check in your lockfile and most importantly USE IT. Always commit package-lock.json or pnpm-lock.yaml. Floating ranges (^, ~, latest) may seem convenient, but they can pull in unreviewed changes without warning. Use npm ci with your lockfile as much as possible and npm install only when necessary.
- Minimize software reuse where possible. Let's face it. The NPM ecosystem will hate this recommendation and the Golang developers will think it's obvious. But sometimes a little copying is better than a LOT of depending. Copying trivial packages means your software doesn't depend on them and reduces supply chain risk.
- Don’t rush updates. A new version isn’t automatically a better version. Give the community a few days to flag malicious or broken releases before upgrading. They call it the bleeding edge for a reason. It can make you bleed.
- Secure your npm account. Turn on 2FA, rotate tokens regularly if you have to use them, and where possible, use OIDC tokens instead of long-lived credentials. This will reduce the risk of long term token theft.
- Watch for red flags. Be wary if a small package suddenly ships a huge change, adds an install script, or changes maintainers without explanation.
These habits are simple but effective. Combined with organizational safeguards, they limit the blast radius of any compromise.
Conclusion
npm attacks aren’t going away. eslint-prettier, Nx, Chalk, Shai-Hulud; it’s always the same script: one stolen account, thousands of downstream victims.
The only way forward is layered defense:
- Security teams: lock dependencies, kill long-lived tokens, bake in cooldowns, and block lifecycle scripts unless there’s no other option.
- Developers: check in your lockfiles, upgrade with suspicion, and question every “harmless” new dependency…especially the tiny ones that suddenly balloon in size.
Supply chain risk is something we implicitly accept when we use open source. We must innovate quickly to survive as a business. So it's not practical or prudent for the vast majority of the world to NOT accept this risk. The teams that thrive will not try to prevent the risk. Our aim should be mitigating it. Prevention is often a fantasy in practice. Educating teams on how to reduce the probability of compromise and building awareness on how to do so should be the ultimate goal of the security industry.



What's next?
When you're ready to take the next step in securing your software supply chain, here are 3 ways Endor Labs can help: