NPM Security Best Practices
Note
The NPM ecosystem is no stranger to compromises12, supply-chain attacks3, malware45, spam6, phishing7, incidents8 or even trolls9. In this repository, I have consolidated a list of information you might find useful in securing yourself against these incidents.
Feel free to submit a Pull Request, or reach out to me on Twitter!
Tip
This repository covers npm, bun, deno, pnpm, yarn and more.
Table of Contents
Got Compromised?
Immediate Actions to take
Caution
In the case of a npm supply chain compromise, here's what you can do immediately:
Identify compromised packages
Keep up with the latest updates about the attack from trusted newsfeed10111213.
Confirm with vulnerability databases like https://security.snyk.io or https://socket.dev/search?e=npm
Remove and replace compromised packages
# remove project cache rm -rf node_modules yarn cache clean pnpm cache delete # remove global cache npm cache clean --force yarn cache clean --mirror bun pm cache rm pnpm store prune
Downgrade and pin dependencies to a known clean version, or remove them entirely.
Remove unwanted node_modules folders from system: cd ~ && npx npkill
Restrict or disable automated scripts
Disable automated pipelines or restrict them while the investigation is ongoing.
Rotate all credentials
Supply chain attacks often targets credentials in the system.
Revoke and regenerate npm tokens, GitHub PATs, SSH keys, and cloud provider credentials.
Monitoring suspicious activities
Review and monitor for unauthorized activities in your projects or organizations. Limit outbound network access to trusted domains only. Limit or revovke access from outsiders or third-party tools. Resume work on a brand new system (highly recommended!)
Pick the best practices below based on your needs to strengthen your system against the next attack.
For Developers
1. Pin Dependency Versions
On
npm, by default, a new dependency will be installed with the Caret^operator. This operator installs the most recentminororpatchreleases. E.g.,^1.2.3will install1.2.3,1.6.2, etc. See https://docs.npmjs.com/about-semantic-versioning and try out the npm SemVer Calculator (https://semver.npmjs.com).
Here's how to pin exact version in various package managers:
npm install --save-exact react pnpm add --save-exact react yarn add --save-exact react bun add --exact react deno add npm:react@19.1.1
We can also update this setting in configuration files (e.g., .npmrc), with either save-exact or save-prefix key and value pairs:
npm config set save-exact=true pnpm config set save-exact true yarn config set defaultSemverRangePrefix ""
For bun, the config file is bunfig.toml and corresponding config is:
Override the transitive dependencies
However, our direct dependencies also have their own dependencies (transitive dependencies). Even if we pin our direct dependencies, their transitive dependencies might still use broad version range operators (like
^or~). The solution is to override the transitive dependencies: https://docs.npmjs.com/cli/v11/configuring-npm/package-json#overrides
In package.json, if we have the following overrides field:
{
"dependencies": {
"library-a": "^3.0.0"
},
"overrides": {
"lodash": "4.17.21"
}
}- Let's assume that
library-a'spackage.jsonhas a dependency on"lodash": "^4.17.0" - Without the
overridessection,npmmight installlodash@4.17.22(or any of the latest4.x.xversions) as a transitive dependency oflibrary-a - However, by adding
"overrides": { "lodash": "4.17.21" }, we are tellingnpmthat anywherelodashappears in the dependency tree, it must be resolved to exactly version4.17.21
For pnpm, we can also define the overrides field in the pnpm-workspace.yaml file: https://pnpm.io/settings#overrides
For yarn, the resolutions field is introduced before the overrides field, and it also offers a similar functionality: https://yarnpkg.com/configuration/manifest#resolutions
{
"resolutions": {
"lodash": "4.17.21"
}
}# yarn also provide a cli to set the resolution: https://yarnpkg.com/cli/set/resolution yarn set resolution <descriptor> <resolution>
For bun, it supports either the overrides field or the resolutions field: https://bun.com/docs/install/overrides
For deno, see denoland/deno#28664 for more details.
2. Include Lockfiles
Ensure to commit package managers lockfiles to
gitand share between different environments14. Different lockfiles are:package-lock.jsonfornpm,pnpm-lock.yamlforpnpm,bun.lockforbun,yarn.lockforyarnanddeno.lockfordeno.In automated environments such as continuous integration and deployments, we should install the exact dependencies as defined in the lockfile.
npm ci bun install --frozen-lockfile yarn install --frozen-lockfile pnpm install --frozen-lockfile deno install --frozen
For deno, we can also set the following in a deno.json file:
{
"lock": {
"frozen": true
}
}Tip
When dealing with merge conflicts in lockfiles, it is not necessary to delete the lockfile. When dependencies (including transitive) are defined with version range operators (^, ~, etc), re-building the lockfile from scratch can result in unexpected updates.
Modern package managers have built-in conflict resolutions1516, just checkout main and re-run install. pnpm also allows Git Branch Lockfiles where it creates a new lockfile based on branch name, and automatically merge it back into the main lockfile later.
3. Disable Lifecycle Scripts
Lifecycle scripts are special scripts that happen in addition to the
pre<event>,post<event>, and<event>scripts. For instance,preinstallis run beforeinstallis run andpostinstallis run afterinstallis run. See how npm handles the "scripts" field: https://docs.npmjs.com/cli/v11/using-npm/scripts#life-cycle-scriptsLifecycle scripts are a common strategy from malicious actors. For example, the "Shai-Hulud" worms3 edit the
package.jsonfile to add apostinstallscript that would then steal credentials.
npm config set ignore-scripts true --global yarn config set enableScripts false
For bun, deno and pnpm, they are disabled by default.
Tip
We can combine many of the flags above. For example, the following npm command would install only production dependencies as defined in the lockfile and ignore lifecycle scripts:
npm ci --omit=dev --ignore-scripts
4. Preinstall Preventions
How do we know and trust that whenever we do
npm install <package-name>, everything will be fine? We shouldn't. Here's how we can ensure that theinstallcommand is safer to run:
Preinstall Scanners
Socket Firewall Free https://socket.dev/blog/introducing-socket-firewall
npm i -g sfw # works for `npm`, `yarn`, `pnpm` sfw npm install <package-name> # example: alias `npm` to `sfw npm` in zsh # echo "alias npm='sfw npm'" >> ~/.zshrc
Aikido Safe Chain https://github.com/AikidoSec/safe-chain
The Aikido Safe Chain wraps around the npm cli, npx, yarn, pnpm, pnpx, bun, bunx, and pip to provide extra checks before installing new packages
npm install -g @aikidosec/safe-chain
npq install express NPQ_PKG_MGR=pnpm npx npq install fastify
With Bun, we can use its Security Scanner API
bun add -d @socketsecurity/bun-security-scanner
From Bun v1.3+, you can integrate Socket with Bun
# in bunfig.toml [install.security] scanner = "@socketsecurity/bun-security-scanner"
Set Minimal Release Age
We can set a delay to avoid installing newly published packages. This applies to all dependencies, including transitive ones. For example,
pnpm v10.16introduced theminimumReleaseAgeoption: https://pnpm.io/settings#minimumreleaseage, which defines the minimum number of minutes that must pass after a version is published before pnpm will install it. IfminimumReleaseAgeis set to1440, then pnpm will not install a version that was published less than 24 hours ago.
npm install --before=2025-10-22 # only install packages published at least 1 day ago npm install --before="$(date -v -1d)" # for Mac or BSD users npm install --before="$(date -d '1 days ago' +%Y-%m-%dT%H:%M:%S%z)" # for Linux users # other related flags: minimumReleaseAgeExclude pnpm config set minimumReleaseAge <minutes> # other related flags: npmPreapprovedPackages yarn config set npmMinimalAgeGate <minutes>
For npm, there is a proposal to add minimumReleaseAge option and minimumReleaseAgeExclude option.
For bun, the minimumReleaseAge and minimumReleaseAgeExcludes options are supported since v1.3.
[install] minimumReleaseAge = 604800 # 7 days in seconds
For deno, they will soon ship a similar feature: denoland/deno#30752
Examples of other tools that offer similar functionality:
npm-check-updates(https://github.com/raineorshine/npm-check-updates) has the--cooldown/-cflag, for example:npx npm-check-updates -i --format group -c 7- Renovate CLI (https://github.com/renovatebot/renovate) has a
minimumReleaseAgeconfig option. - Step Security (https://www.stepsecurity.io) has a NPM Package Cooldown Check feature.
5. Runtime Protections
Most techniques focus on the install and build phases, we can add an extra layer of security during the runtime phase of JavaScript applications.
Permission Model
In the latest LTS version of
nodejs, we can use the Permission model to control what system resources a process has access to or what actions the process can take with those resources. However, this does not provide security guarantees in the presence of malicious code. Malicious code can still bypass the permission model and execute arbitrary code without the restrictions imposed by the permission model.
Read about the Node.js permission model: https://nodejs.org/docs/latest/api/permissions.html
# by default, granted full access node index.js # restrict access to all available permissions node --permission index.js # enable specific permissions node --permission --allow-fs-read=* --allow-fs-write=* index.js # use permission model with `npx` npx --node-options="--permission" <package-name>
Deno disables permissions by default. See https://docs.deno.com/runtime/fundamentals/security/
# by default, restrict access deno run script.ts # enable specific permission deno run --allow-read script.ts
For Bun, the permission model is currently discussed here and here.
Hardened JavaScript
Companies like MetaMask and Moddable uses https://www.npmjs.com/package/ses and https://github.com/LavaMoat/LavaMoat to enable runtime protections like prevent modifying JavaScript's primordials (Object, String, Number, Array, ...), and limit access to the platform API (window, document, XHR, etc) per-package. These mechanism are also suggested as TC39 proposals like https://github.com/tc39/proposal-compartments
Watch The Attacker is Inside: Javascript Supplychain Security and LavaMoat (~20mins, Nov 2022) to get a quick high level overview of how this works.
6. Reduce External Dependencies
Because
npmhas a low barrier for publishing packages, the ecosystem quickly grew to be the biggest package registry with over 5 million packages to date17. But not all packages are created equal. There are small utility packages8 that are downloaded as dependencies when we could write them ourselves and raise the question of "have we forgotten how to code?18"
Between nodejs, bun and deno, developers can use many of their modern features instead of relying on third-party libraries. The native modules may not provide the same level of functionality, but they should be considered whenever possible. Here are few examples:
| NPM libraries | Built-in modules |
|---|---|
axios, node-fetch, got, etc |
nativefetch API |
jest, mocha, ava, etc |
node:test,node:assert, bun test and deno test |
nodemon, chokidar, etc |
node --watch, bun --watch and deno --watch |
dotenv, dotenv-expand, etc |
node --env-file, bun --env-file and deno --env-file |
typescript, ts-node, etc |
node --experimental-strip-types19, native to deno and bun |
esbuild, rollup, etc |
bun build and deno bundle |
prettier, eslint, etc |
deno lint and deno fmt |
Here are some resources that you might find useful:
- https://obsidian.md/blog/less-is-safer
- https://kashw1n.com/blog/nodejs-2025
- https://lyra.horse/blog/2025/08/you-dont-need-js
- https://blog.greenroots.info/10-lesser-known-web-apis-you-may-want-to-use
- https://github.com/you-dont-need/You-Dont-Need-Momentjs
- Visualise library dependencies: https://npmgraph.js.org
- Another tool to visualise dependencies and more: https://node-modules.dev
- Knip (remove unused dependencies): https://github.com/webpro-nl/knip
- Erase unwanted
node_moduleswithnpkill:cd ~ && npx npkill
For Maintainers
7. Enable 2FA
https://docs.npmjs.com/about-two-factor-authentication
Two factor authentication (2FA) adds an extra layer of authentication to your
npmaccount. 2FA is not required by default, but from December 2025, when you create a new package, 2FA will be enabled by default in the package settings.
# ensure that 2FA is enabled for auth and writes (this is the default)
npm profile enable-2fa auth-and-writes| Automation level | Package publishing access |
|---|---|
| Manual | Set each package access to Require 2FA and Disable Tokens |
| Automatic | Set each package access to Require two-factor authentication OR Single factor automation tokens OR Single factor granular access tokens |
Important
It is advised to configure a security-key that support WebAuthn, instead of time-based one-time password (TOTP)20
8. Create Tokens with Limited Access
Tip
Best practice: prefer trusted publishing over tokens if possible! See the "trusted publishing" section below for more details.
https://docs.npmjs.com/about-access-tokens#about-granular-access-tokens
At the end of 2025, NPM announced the sunset of Legacy Tokens to improve security. Granular Access Tokens is the default going forward21.
Create granular access tokens via the website: https://docs.npmjs.com/creating-and-viewing-access-tokens#creating-granular-access-tokens-on-the-website or npm cli: https://docs.npmjs.com/cli/v11/commands/npm-token
The npm login cli command enables a two-hour session token instead of long-lived tokens. During these sessions, 2FA is enforced for publishing operations, adding an extra layer of security.
Here are some best practices when creating tokens:
- Descriptive token names
- Restrict token to specific packages, scopes, and organizations
- Set a token expiration date (e.g., annually)
- Limit token access based on IP address ranges (CIDR notation)
- Select between read-only or read and write access
- Don't use the same token for multiple purposes
9. Generate Provenance Statements
https://docs.npmjs.com/generating-provenance-statements
The provenance attestation is established by publicly providing a link to a package's source code and build instructions from the build environment. This allows developers to verify where and how your package was built before they download it.
The publish attestations are generated by the registry when a package is published by an authorized user. When an npm package is published with provenance, it is signed by Sigstore public good servers and logged in a public transparency ledger, where users can view this information.
For example, here's what a provenance statement look like on the
vuepackage page: https://www.npmjs.com/package/vue#provenance
To establish provenance, use a supported CI/CD provider (e.g., GitHub Actions) and publish with the correct flag:
To publish without evoking the npm publish command, we can do one of the following:
- Set
NPM_CONFIG_PROVENANCEtotruein CI/CD environment - Add
provenance=trueto.npmrcfile - Add
publishConfigblock topackage.json
"publishConfig": { "provenance": true }
For those interested in Reproducible Builds, check out OSS Rebuild (https://github.com/google/oss-rebuild) and the Supply-chain Levels for Software Artifacts (SLSA) framework (https://slsa.dev).
Trusted Publishing
Use trusted publishing over tokens whenever possible20
When using OpenID Connect (OIDC) auth, one can publish packages without npm tokens, and get automatic provenance. This is called trusted publishing and read the GitHub announcement here: https://github.blog/changelog/2025-07-31-npm-trusted-publishing-with-oidc-is-generally-available/
See https://docs.npmjs.com/trusted-publishers for instructions on how to configure trusted publishing.
Related tools:
- https://github.com/antfu/open-packages-on-npm (CLI to setup Trusted Publisher for monorepo packages)
- https://github.com/sxzz/userscripts/blob/main/src/npm-trusted-publisher.md (Userscript to fill the form for Trusted Publisher on npmjs.com)
10. Review Published Files
Limiting the files in an npm package helps prevent malware by reducing the attack surface, and it avoids accidental leaking of sensitive data
The files field in package.json is used to specify the files that should be included in the published package. Certain files are always included, see: https://docs.npmjs.com/cli/v11/configuring-npm/package-json#files for more details.
{
"name": "my-package",
"version": "1.0.0",
"main": "dist/index.js",
"files": ["dist", "LICENSE", "README.md"]
}Tip
The .npmignore file can also be used to exclude files from the published package. It will not override the "files" field, but in subdirectories it will.
The .npmignore file works just like a .gitignore. If there is a .gitignore file, and .npmignore is missing, .gitignore's contents will be used instead.
Run npm pack --dry-run or npm publish --dry-run to see what would happen when we run the pack or publish command.
> npm pack --dry-run
npm notice Tarball Contents
npm notice 1.1kB LICENSE
npm notice 1.9kB README.md
npm notice 108B index.js
npm notice 700B package.json
npm notice Tarball DetailsIn deno.json, use the publish.include and publish.exclude fields to specify the files that should be included or excluded:
{
"publish": {
"include": ["dist/", "README.md", "deno.json"],
"exclude": ["**/*.test.*"]
}
}Miscellaneous
11. NPM Organization
https://docs.npmjs.com/organizations
At the organization level, best practices are:
- Enable
Require 2FAat the Organization Level - Minimise the number of
npmOrganization members - If multiple package teams in same organization, set the
developersTeam permission for all packages toREAD - Create separate Teams to manage permissions for each package
12. Alternative Registry
JSR is a modern JavaScript/TypeScript package registry with backwards compatibility with npm.
deno add jsr:<package-name> pnpm add jsr:<package-name> # pnpm 10.9+ yarn add jsr:<package-name> # yarn 4.9+ # npm, bun, and older versions of yarn or pnpm npx jsr add <package-name> # replace npx with yarn dlx, pnpm dlx, or bunx
Private Registry
Private package registries are a great way for organizations to manage their own dependencies, acts as a proxy to the public
npmregistry, and enforce security policies before they are used in a project.
Here are some private registries that you might find useful:
- GitHub Packages https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-npm-registry
Important
Currently, GitHub Packages only supports personal access token (classic), but classic PATs can be insecure as it has broad permissions and lacks of granular permissions!2223 For this reason, you may want to pick an alternative package registry from below ⬇️
- Verdaccio https://github.com/verdaccio/verdaccio
- See Verdaccio best practices: https://verdaccio.org/docs/best/
- Vlt https://www.vlt.sh/
- vlt’s Serverless Registry (VSR) can be deployed to Cloudflare Workers in minutes.
- JFrog Artifactory https://jfrog.com/integrations/npm-registry
- Sonatype: https://help.sonatype.com/en/npm-registry.html
Tip
No Registry?
If the usage of a public registry like npm is a real concern, it is also possible to build and import the library yourself as long as you have access to the source code.
See https://boda.sh/blog/pnpm-workspace-git-submodules/ for adding packages without npm but with pnpm workspace and git submodules.
13. Audit, Monitor and Security Tools
Audit
Many package managers provide audit functionality to scan your project's dependencies for known security vulnerabilities, show a report and recommend the best way to fix them.
npm audit # audit dependencies npm audit fix # automatically install any compatible updates npm audit signatures # verify the signatures of the dependencies pnpm audit pnpm audit --fix bun audit yarn npm audit yarn npm audit --recursive # audit transitive dependencies
GitHub
GitHub offers several services that can help protect against npm malwares, including:
- Dependabot: This tool automatically scans your project's dependencies, including
npmpackages, for known vulnerabilities. - Software Bill of Materials (SBOMs): GitHub allows you to export an SBOM for your repository directly from its dependency graph. An SBOM provides a comprehensive list of all your project's dependencies, including transitive ones (dependencies of your dependencies).
- Code Scanning: Code scanning can also help identify potential vulnerabilities or suspicious patterns that might arise from integrating compromised
npmpackages.
OpenSSF Scorecard
https://securityscorecards.dev and https://github.com/ossf/scorecard
Free and open source automated tool that assesses a number of important heuristics ("checks") associated with software security and assigns each check a score of 0-10. Several risks mentioned in this repository are included as part of the checks: Pinned Dependencies, Token Permissions, Packaging, Signed Releases,...
Run the checks:
- automatically on code you own using the GitHub Action
- manually on your (or somebody else’s) project via the Command Line
Socket.dev
Socket.dev is a security platform that protects code from both vulnerable and malicious dependencies. It offers various tools such as a GitHub App scans pull requests, CLI tool, web extension, VSCode extension and more. Here's their talk on AI powered malware hunting at scale, Jan 2025. Plus the Socket Firewall sfw tool in the Preinstall Scanners section.
Snyk
Snyk offers a suite of tools to fix vulnerabilities in open source dependencies, including a CLI to run vulnerability scans on local machine, IDE integrations to embed into development environment, and API to integrate with Snyk programmatically. For example, you can test public npm packages before use or create automatic PRs for known vulnerabilities.
FOSSA
FOSSA is a compliance and security platform that helps organizations manage the complexities of their software supply chain. It achieves this by providing visibility into all software components, from packages and containers to binaries. By generating comprehensive SBOMs (Software Bill of Materials), companies reduce legal and IP risk, consolidate vulnerability management across their codebase, and comply with regulatory reporting requirements.
14. Support OSS
Maintainer burnout is a significant problem in the open-source community. Many popular
npmpackages are maintained by volunteers who work in their spare time, often without any compensation. Over time, this can lead to exhaustion and a lack of motivation, making them more susceptible to social engineering where a malicious actor pretends to be a helpful contributor and eventually injects malicious code.
In 2018, the
event-streampackage was compromised due to the maintainer giving access to a malicious actor24. Another example outside the JavaScript ecosystem is the XZ Utils incident25 in 2024 where a malicious actor worked for over three years to attain a position of trust.
OSS donations also help create a more sustainable model for open-source development. Foundations can help support the business, marketing, legal, technical assistance and direct support behind hundreds of open source projects that so many rely upon2627.
In the JavaScript ecosystem, the OpenJS Foundation (https://openjsf.org) was founded in 2019 from a merger of JS Foundation and Node.js Foundation to support some of the most important JS projects. And few other platforms are listed below where you can donate and support the OSS you use everyday:
- GitHub Sponsors https://github.com/sponsors
- Open Collective https://opencollective.com
- Thanks.dev https://thanks.dev
- Open Source Pledge https://opensourcepledge.com
- Ecosystem Funds: https://funds.ecosyste.ms
Star History
-
https://www.aikido.dev/blog/npm-debug-and-chalk-packages-compromised ↩
-
https://socket.dev/blog/ongoing-supply-chain-attack-targets-crowdstrike-npm-packages ↩ ↩2
-
https://www.reversinglabs.com/blog/malicious-npm-patch-delivers-reverse-shell ↩
-
https://socket.dev/blog/north-korean-apt-lazarus-targets-developers-with-malicious-npm-package ↩
-
https://github.com/duckdb/duckdb-node/security/advisories/GHSA-w62p-hx95-gf2c ↩
-
https://nesbitt.io/2025/12/06/github-actions-package-manager.html#:~:text=The%20fix%20is%20a%20lockfile ↩
-
https://stackoverflow.com/questions/54124033/deleting-package-lock-json-to-resolve-conflicts-quickly ↩
-
https://docs.npmjs.com/trusted-publishers#prefer-trusted-publishing-over-tokens ↩ ↩2
-
https://github.blog/changelog/2025-12-09-npm-classic-tokens-revoked-session-based-auth-and-cli-token-management-now-available/ ↩
-
https://docs.github.com/en/packages/learn-github-packages/about-permissions-for-github-packages#about-scopes-and-permissions-for-package-registries ↩
-
https://openssf.org/blog/2024/04/15/open-source-security-openssf-and-openjs-foundations-issue-alert-for-social-engineering-takeovers-of-open-source-projects/ ↩
