Attacks against popular npm packages used in development pose a risk to many organizations. Lately, several high-profile supply-chain attacks have happened.

Npm Supply-Chain Attacks: How to Reduce Risk

The npm package ecosystem is a cornerstone in modern software development. For example, it’s widely popular for both frontend and backend web development. When you build a moderately sized project, you end up depending on hundreds of packages, possibly from hundreds of publishers.
At the same time, each package could theoretically execute code on developer devices, in build systems (CI/CD), on backend systems and in user’s browsers. All potentially with malicious intent, endangering thousands of organizations.

Securely sourcing the packages and managing the looming risk of some maintainer being breached becomes critical. Lately, an alarming amount of sophisticated supply-chain attacks have happened against the npm ecosystem (such as the recent Shai-Hulud attack and attack on nx).

In this article, we share practical, technical steps you can take to reduce your risk from malicious npm packages, based on providing support for customers in these matters for many years.

The Importance of npm Package Security 

Npm supply-chain attacks have been a hot topic lately, with several attacks having security teams scramble to investigate. The npm package ecosystem is deeply embedded in software development for projects that use JavaScript/TypeScript. 

You don’t just benefit from npm when developing frontend code, it’s also popular for things such as: 

  • Backend code (Node.js and similar frameworks) 
  • Build systems and CI/CD pipelines 
  • Infrastructure as Code (e.g., AWS CDK) 

It feels like npm is everywhere. On top of that, even if a project just has tens of declared dependencies, each such dependency typically has tens or hundreds of dependencies as well (“transitive dependencies”). In total you can easily end up with more than a thousand dependencies, coming from hundreds of separate publishers whose operational security you depend on. A breach of any publisher can compromise your development and production systems. 

Malware delivered through npm packages could execute in several different stages, such as: 

  • When the developer or CI/CD installs the package (typically the “postinstall” step). 
  • In build and packaging steps on the developer device or in CI/CD. 
  • On a backend system that uses the package (for example a NodeJs application). 
  • In a desktop application in the case of for example Electron. 
  • In the user’s web browser in the case of a frontend app (possibly leading to token theft and similar). 
Supply-chain attack paths for npm usage in development illustrated. A diagram illustrating the connections made from a developer machine. To the package repository, source code repository and CI/CD or build system. But also the fact that the developer often has credentials to production servivces, cloud APIs and other services from their device. If malware gets access to the developer machine through an npm package, all of these are at risk.
Malware delivered from a package repository can directly or indirectly affect many sensitive systems in the development process, as illustrated by this image showing a typical deploy scenario for a cloud-based web app. The developer’s computer often has sensitive secrets and identities, as does the build environment (CI/CD) and the production environment. A malicious party can often use these secrets to access other services than the ones needed for the development context.

What the Malware Might Do 

Npm supply-chain attacks often take the form of “infostealers”, where they focus on stealing relevant secrets such as keys and then exfiltrate those back to the threat actor. This is typically efficient for attackers since the number of infected systems could become large, and the infected systems often have persistently usable secret keys for cloud environments and similar. In the case of the Shai-Hulud malware, it even specifically focused on harvesting npm publishing credentials in order to automatically spread itself to other packages. 

Another common strategy for malware is to focus on either stealing or mining cryptocurrency. In the former case it is likely with specific targets in mind (other users becoming collateral damage or more opportunistic targets). 

Threat actors could of course deliver malware through npm that would persist locally, call out to Command-and-Control servers and otherwise behave as typical ransomware-focused malware. But as long as secret keys are spread and readily available across environments, infostealer behavior is likely tempting and provides good value to threat actors with relatively small effort. In a modern cloud environment, that AWS Secret key you have locally provides instant privilege escalation to an interesting target. 

What You Can Do to Reduce the Risk of Becoming a Victim to npm supply-chain attacks

As usual when modeling risk we consider two main components: probability and impact. 

  • How can we reduce the probability of one of our dependencies being malicious or vulnerable due to npm supply-chain attacks? 
  • How can we reduce the impact of such an event, under the very real assumption that such events can happen. 

Most of these recommendations help you reduce risk for other scenarios as well, such as typo-squatting and dependency confusion attacks. 

We’ll go into 11 mitigation categories that can improve your resilience and overall npm package security:  

  1. Spring cleaning of dependencies. Remove what you don’t need or can’t trust (reducing probability). 
  2. Know what dependencies you’ve declared. Make sure your SCA can see all packages (reducing probability and impact) 
  3. Don’t run install scripts automatically (reducing impact) 
  4. Lock the lockfile. Don’t upgrade packages implicitly (reducing probability) 
  5. Cool down with the updates. Introduce package quarantining (reducing probability) 
  6. Know what you have downloaded. Use a package proxy with audit logs (reducing impact and probability) 
  7. Enforce branch and environment protections in CI/CD to keep the problem local (Reducing impact) 
  8. Pin container images. Lock down what your base images depend on (reducing probability) 
  9. Reduce/remove persistent key usage (reducing impact) 
  10. Reduce overall privilege of developer machines (reducing impact) 
  11. Have a clear incident response plan (reducing impact) 

These mitigations are protections you can implement in your organization. The first eight have a relatively clear “definition of done” and scope, whereas the last three are more a matter of continuous work and policy. 

Mitigation 1: Spring cleaning of dependencies 

Every dependency is a potential risk (and can bring fantastic features as well). Regularly review your package.json and remove what you no longer need (ideally with the help of automatic tooling). Remove packages that are not maintained as well as packages where you don’t trust the maintainer. You can likely drop several dependencies without much of a hassle. 

Mitigation 2: Know what dependencies you’ve declared 

Having Software Composition Analysis (SCA) is essential to supply-chain security. Tooling or processes for SCA provide inventory of the dependencies that you are directly or indirectly using. To react to known vulnerabilities and malicious packages, you need to at least know which dependencies you have in your repositories. 

If you have SCA, investigate that the coverage is sufficient. As an example, at the time of writing Dependabot does not support the package lock format of the package manager “bun” well enough to be able to see it in the dependency graph or get Dependabot alerts. If you use bun for your JavaScript project on Github, your visibility is too low unless you have other tooling to compensate. 

Mitigation 3: Don’t run install scripts automatically 

Many npm supply-chain attacks rely on package manager features that run scripts in the install process (true also for some other package managers). This is inherently dangerous since a package that is for example meant to just bundle JavaScript for a client web browser will get code execution already on the developer machine and build environment upon install. 

How to achieve this depends on the tooling. It’s usually complex to fully disable scripts (--ignore-scripts and similar), as many projects rely on the functionality, but if you can it’s great. 

Make sure to test this thoroughly, as disabling scripts could break functionality 

Pnpm from version 10 by default disables scripts from dependencies, which is a very welcome default setting. If you’re using pnpm before version 10 you can set the configuration ignoreDepScripts to allow your main project scripts, but not scripts from dependencies. 

Bun has a similar default, but with an allow-list for popular projects (leaving some risk, but reducing greatly) . 

Mitigation 4: Lock the lockfile. Don’t upgrade packages implicitly 

It’s very common in modern package managers to have a “lockfile”. You define your direct dependencies in a package definition file (such as package.json), and then the package manager resolves and downloads all transitive dependencies and updates the lockfile (such as package-lock.json or pnpm-lock.yaml) to point to the total set of packages that is needed. Most of the time when you build a project, you don’t want the lockfile to change (packages shouldn’t suddenly be updated). At the same time, many package definitions use relative versions (~ and ^), meaning that it’s easy to end up downloading new packages and updating packages inadvertently for example when building in CI. 

Consider using commands that do not allow updating lockfiles when building in CI. For example, npm ci instead of npm install or pnpm --frozen-lockfile. Do note that these commands can be problematic to use in the dev environments, for cases where they force bypass of npm module caches. As a variant, one can also consider pnpm’s --offline and --prefer-offline flags in cases where caches are generally already primed. 

Note: “pinning” dependencies to specific versions is difficult in npm, since you usually have such a complex dependency tree, and transitive dependencies will not usually be pinnable regardless. But it’s still often a good idea to consider pinning direct dependencies regardless, since it’s good to have a more consistent dependency tree across development and build environments. 

Mitigation 5: Cool down with the updates 

In some cases, you can configure the package tooling to not install packages until after they have already existed at the external repository for some predefined time. The idea is that many npm supply-chain attacks are discovered within hours. Hopefully you can avoid them by putting the packages in quarantine for a while. 

Renovate has had this feature for years. It has recently also been introduced to GitHub Dependabot and pnpm as minimumReleaseAge

It is a good idea to consider cooldown but make sure to test how it relates to your processes for patching known vulnerabilities. You should make sure you have ways to override delays in case you quickly need to install a non-vulnerable version of something that was recently patched. 

One typical case where exclusions would be relevant is if you have internal packages. It is likely that you want to be able to immediately reference company-internal packages after publishing (not wait for hours). 

Mitigation 6: Know what has been downloaded. Use a package proxy with audit logs 

Most package systems, including npm, support using an organization local package source/proxy. You can set up an npm compatible proxy and configure all developer devices and build systems to always download packages through that proxy. When the developers download a package, it goes through the proxy (with a clear trace and control point). 

By doing so, you gain visibility into what packages have actually been installed in the organization. In many cases with supply-chain attacks, the most likely place for an installation is on some developer machine. In those cases, the central software composition analysis tooling is blind to the installation. 

It is usually good enough to make sure all devices have overridden the central repository in their config, but if you want to take it to the next level you can consider blocking outbound requests to the central repos from the developer devices. 

As a bonus, you can often configure proxies so they block download of certain packages or perform security analysis on the packages being downloaded. Beyond that, endpoint detection on the proxy could in some cases analyze packages on disk to further flag known or likely malicious packages. 

Mitigation 7: Enforce branch and environment protections in CI/CD 

Someone could steal a developer’s access towards CI/CD. We must make sure that such an event does not mean that production environments can be breached. You should configure CI/CD environments so that deployment secrets are isolated and only accessible to protected deployment processes. See our article on GitHub supply-chain security for more information. 

This is extra relevant since several of the recent npm supply-chain attacks have in fact involved abusing the developers GitHub environment, and in many cases, they seem to have utilized weaknesses in the GitHub workflow security of open-source projects. 

Mitigation 8: Pin container images 

What does container images have to do with anything?”, you might ask. Could be quite a bit! 
Many container images you depend on will not force version locking of their dependencies in the build steps of the Dockerfile. If you in turn are referencing the images by a mutable/changing tag, you may end up with the malicious npm dependencies in your app (say if you reference :latest, or in some cases a major version like :7)

As usual in application security, this is something to weigh. For certain framework images and publishers, it can be a feature to be able to quickly rebuild to get fixes for known vulnerabilities. But to do so, you may want to investigate that publisher’s protections from supply-chain attacks. 

Mitigation 9: Reduce/remove persistent key usage 

Strive to have as few secret keys defined in files or environment variables as possible. In case of an infostealer, these are at risk. Use temporary tokens if you can, with bonus points if you can bind them to your device, expected outbound IP address or similar. One way to reduce risk could be to fetch keys when needed from a password manager or say a cloud secrets manager. 

Specifically, for npm publishing tokens, never have them on disk or in environment variables if you can avoid it. Use OpenID Connect based publishing flows instead of token based ones and do deployment in a controlled environment (like a CI/CD process), not from your developer environment. 

As usual though, the best thing is if you can make sure, they are never even there. But if you need tokens, at least make sure they are as low-privilege as possible. 

The same thing applies to the CI/CD-environment. For specific details and recommendations on this in a GitHub setting, see our earlier post on the Truesec blog

Mitigation 10: Reduce overall privilege of developer machines 

This is a big one and deserves a blog series on its own. The basic idea is that we need to make developer environments less privileged. 

Assume that malware might execute and reduce its potential impact by: 

  • Separating the privileged admin flows from daily access flows. Don’t keep tokens for cloud admin accounts in the same environment as you are doing development.
  • Use temporary, scoped down tokens for access to reduce blast radius 
  • Isolate development environments using for example dev containers, virtual machines or even cloud-based development environments. Make it so it’s difficult to escalate privilege from the development context. 
  • Make your access to CI/CD less privileged. When you need to configure high privilige settings like deploy tokens, use separate contexts. If a threat actor steals your GitHub tokens that you use for daily development activites, they must not be able to use them to publish to production (see also our separate post around this).
  • Have monitoring of development devices, such as Endpoint Detection and Response (EDR) to more easily react and isolate machines with suspicious behaviour (such as infostealers). 
  • ..and other general least privilege and privileged access pattern of reducing blast radius. 

Mitigation 11: Have a clear incident response plan 

You must have a clear incident process to be able to quickly and methodically assess the situation and take the right actions if you are affected. Time is of the essence in a breach scenario. Don’t make planning and organization eat up that valuable time. 

Final Words 

Npm supply-chain attacks are a growing threat, but with careful dependency management, and layered mitigations, organizations can significantly reduce their risk. By treating dependencies as part of your attack surface and maintaining a healthy security posture, you can continue to benefit from the npm ecosystem while minimizing exposure to malicious packages. 

Key takeaways: 

  • Regularly audit and minimize your dependencies 
  • Use SCA tools and package proxies for visibility 
  • Harden your CI/CD and developer environments against compromise. Both reducing the likelihood of compromise and the impact in case it happens. 

If you need help designing your supply-chain security strategy, contact Truesec

For more information on this topic, see our previous blog series on supply-chain risk and mitigations (part 1 , part 2). 

Stay ahead with cyber insights

Newsletter

Stay ahead in cybersecurity! Sign up for Truesec’s newsletter to receive the latest insights, expert tips, and industry news directly to your inbox. Join our community of professionals and stay informed about emerging threats, best practices, and exclusive updates from Truesec.