NPM hasn’t had a “quiet week” in a while has it?

Recent hits
[eslint-prettier](https://www.endorlabs.com/learn/cve-2025-54313-eslint-config-prettier-compromise----high-severity-but-windows-only),
[Nx](https://www.endorlabs.com/learn/nx-build-platform-compromised-by-supply-chain-attack---how-attackers-collude-with-ai-code-assistants), and
[chalk](https://www.endorlabs.com/learn/major-supply-chain-attack-compromises-popular-npm-packages-including-chalk-and-debug)
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](https://www.endorlabs.com/learn/npm-malware-outbreak-tinycolor-and-crowdstrike-packages-compromised?hsCtaAttrib=195723435125): 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](https://www.usenix.org/system/files/sec19-zimmermann.pdf)” 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](https://docs.npmjs.com/trusted-publishers)
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](https://docs.npmjs.com/cli/v8/using-npm/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](https://docs.npmjs.com/generating-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](https://www.nodejs-security.com/blog/npm-ignore-scripts-best-practices-as-security-mitigation-for-malicious-packages)
  /
  [yarn](https://classic.yarnpkg.com/lang/en/docs/cli/install/#toc-yarn-install-ignore-scripts)
  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](https://github.com/pnpm/pnpm/pull/8897)
  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](https://www.endorlabs.com/use-cases/malware-detection)
  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](https://www.cisecurity.org/benchmark/software-supply-chain-security)
  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](https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference)
  allow you to reduce the probability that you update to a newly published
  and potentially malicious version of a software package. Take advantage of
  Package Manager features for delaying package adoption. PNPM recently
  released a feature for establishing
  [a minimum release age](https://pnpm.io/supply-chain-security#delay-dependency-updates), and npm install accepts a flag,
  [--before](https://docs.npmjs.com/cli/v8/using-npm/config#before), to only accept packages released after a certain date. (Update: After
  publication, Yarn added a comparable setting:
  [npmMinimalAgeGate](https://yarnpkg.com/configuration/yarnrc#npmMinimalAgeGate))
* **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](https://github.com/micromatch/to-regex-range/pull/17)
  and the
  [Golang developers will think it's obvious.](https://www.efekarakus.com/2021/09/23/a-little-copying-is-better-than-a-little-dependency.html)
  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.