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.
Tip
Here's a sample .npmrc
file with the config options mentioned below:
ignore-scripts=true
provenance=true
save-exact=true
save-prefix=''
And other configuration files examples are here:
On
npm
, by default, a new dependency will be installed with the Caret^
operator. This operator installs the most recentminor
orpatch
releases. E.g.,^1.2.3
will 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:[email protected]
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:
[install]
exact = true
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.json
has a dependency on"lodash": "^4.17.0"
- Without the
overrides
section,npm
might install[email protected]
(or any of the latest4.x.x
versions) as a transitive dependency oflibrary-a
- However, by adding
"overrides": { "lodash": "4.17.21" }
, we are tellingnpm
that anywherelodash
appears 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.
Ensure to commit package managers lockfiles to
git
and share between different environments. Different lockfiles are:package-lock.json
fornpm
,pnpm-lock.yaml
forpnpm
,bun.lock
forbun
,yarn.lock
foryarn
anddeno.lock
fordeno
.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
deno install --frozen
For deno
, we can also set the following in a deno.json
file:
{
"lock": {
"frozen": true
}
}
Lifecycle scripts are special scripts that happen in addition to the
pre<event>
,post<event>
, and<event>
scripts. For instance,preinstall
is run beforeinstall
is run andpostinstall
is run afterinstall
is 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.json
file to add apostinstall
script 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.
Note
For bun
, the top 500 npm packages with lifecycle scripts are allowed 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
We can set a delay to avoid installing newly published packages. This applies to all dependencies, including transitive ones. For example,
pnpm v10.16
introduced theminimumReleaseAge
option: 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. IfminimumReleaseAge
is set to1440
, then pnpm will not install a version that was published less than 24 hours ago.
pnpm config set minimumReleaseAge <minutes>
# only install packages published at least 1 day ago
npm install --before="$(date -v -1d)"
yarn config set npmMinimalAgeGate <minutes>
For pnpm
, there's also a minimumReleaseAgeExclude
option to exclude certain packages from the minimum release age.
For npm
, there is a proposal to add minimumReleaseAge
option and minimumReleaseAgeExclude
option.
For yarn
, config options npmMinimalAgeGate
and npmPreapprovedPackages
are implemented since v4.10.0
.
For bun
, it is discussed here: oven-sh/bun#22679
For deno
, an draft proposal is here: denoland/deno#30752
Examples of other tools that offer similar functionality:
- npm-check-updates (https://github.com/raineorshine/npm-check-updates) has the
--cooldown
flag. - Renovate CLI (https://github.com/renovatebot/renovate) has a
minimumReleaseAge
config option. - Step Security (https://www.stepsecurity.io) has a NPM Package Cooldown Check feature.
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 enables 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.
Because
npm
has a low barrier for publishing packages, the ecosystem quickly grew to be the biggest package registry with over 5 million packages to date10. 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?11"
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-types 12, 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 NPM dependencies: https://npmgraph.js.org
- Knip (remove unused dependencies): https://github.com/webpro-nl/knip
https://docs.npmjs.com/about-two-factor-authentication
Two factor authentication (2FA) adds an extra layer of authentication to your
npm
account. 2FA is not required by default, but it is a good practice to enable it.
# 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)13
https://docs.npmjs.com/about-access-tokens#about-granular-access-tokens
An access token is a common way to authenticate to
npm
when using the API or thenpm
CLI.
npm token create # for a read and publish token
npm token create --read-only # for a read-only token
npm token create --cidr=[list] # for a CIDR-restricted read and publish token
npm token create --read-only --cidr=[list] # for a CIDR-restricted read-only token
Important
Granular Access Tokens should be used instead of Legacy Tokens. Legacy tokens cannot be scoped and don't automatically expire. They're considered dangerous to use.
- 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
- Descriptive token names
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
vue
package 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:
npm publish --provenance
To publish without evoking the npm publish
command, we can do one of the following:
- Set
NPM_CONFIG_PROVENANCE
totrue
in CI/CD environment - Add
provenance=true
to.npmrc
file - Add
publishConfig
block 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).
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/ and https://docs.npmjs.com/trusted-publishers
Important
It is recommended to use trusted publishing instead of tokens13.
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 Details
In 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.*"]
}
}
https://docs.npmjs.com/organizations
At the organization level, best practices are:
- Enable
Require 2FA
at the Organization Level - Minimise the number of
npm
Organization members - If multiple package teams in same organization, set the
developers
Team permission for all packages toREAD
- Create separate Teams to manage permissions for each package
Private package registries are a great way for organizations to manage their own dependencies, and can acts as a proxy to the public
npm
registry. Organizations can enforce security policies and vet packages 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
- Verdaccio https://github.com/verdaccio/verdaccio
- Vlt https://www.vlt.sh/
- JFrog Artifactory https://jfrog.com/integrations/npm-registry
- Sonatype: https://help.sonatype.com/en/npm-registry.html
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
https://github.com/security
GitHub offers several services that can help protect against npm
malwares, including:
- Dependabot: This tool automatically scans your project's dependencies, including
npm
packages, 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
npm
packages.
Warning
If you spot vulnerabilities or issues in NPM or Github, please report them using the following links:
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.
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.
Maintainer burnout is a significant problem in the open-source community. Many popular
npm
packages 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-stream
package was compromised due to the maintainer giving access to a malicious actor14. Another example outside the JavaScript ecosystem is the XZ Utils incident15 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 upon1617.
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
Footnotes
-
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://github.blog/security/supply-chain-security/our-plan-for-a-more-secure-npm-supply-chain ↩ ↩2
-
https://github.com/dominictarr/event-stream/issues/116 ↩
-
https://openssf.org/blog/2024/04/15/open-source-security-openssf-and-openjs-foundations-issue-alert-for-social-engineering-takeovers-of-open-source-projects/ ↩