Skip to content

bodadotsh/npm-security-best-practices

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

28 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

npm meme

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.

hn discussion

Table of Contents

For Developers

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:

1. Pin Dependency Versions

On npm, by default, a new dependency will be installed with the Caret ^ operator. This operator installs the most recent minor or patch releases. E.g., ^1.2.3 will install 1.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

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's ⁠package.json has a dependency on "lodash": "^4.17.0"
  • Without the ⁠overrides section, ⁠npm might install [email protected] (or any of the latest ⁠4.x.x versions) as a transitive dependency of ⁠library-a
  • However, by adding "overrides": { "lodash": "4.17.21" }, we are telling ⁠npm that anywhere ⁠lodash appears in the dependency tree, it must be resolved to exactly version ⁠4.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 git and share between different environments. Different lockfiles are: package-lock.json for npm, pnpm-lock.yaml for pnpm, bun.lock for bun, yarn.lock for yarn and deno.lock for deno.

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
  }
}

3. Disable Lifecycle Scripts

Lifecycle scripts are special scripts that happen in addition to the pre<event>, post<event>, and <event> scripts. For instance, preinstall is run before install is run and postinstall is run after install is run. See how npm handles the "scripts" field: https://docs.npmjs.com/cli/v11/using-npm/scripts#life-cycle-scripts

Lifecycle scripts are a common strategy from malicious actors. For example, the "Shai-Hulud" worms3 edit the package.json file to add a postinstall 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

4. 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.16 introduced the minimumReleaseAge 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. If minimumReleaseAge is set to 1440, 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:

5. 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 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.

6. Reduce External Dependencies

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-types12, 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:

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 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

8. Create Tokens with Limited Access

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 the npm 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

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 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 to true in CI/CD environment
  • Add provenance=true to .npmrc file
  • Add publishConfig block to package.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

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.

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 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.*"]
  }
}

Miscellaneous

11. NPM Organization

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 to READ
  • Create separate Teams to manage permissions for each package

12. Use Private Registry

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:

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

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.

Socket.dev

https://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.

Snyk

https://snyk.io

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.

14. Support OSS

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:

Footnotes

  1. https://www.aikido.dev/blog/npm-debug-and-chalk-packages-compromised

  2. https://socket.dev/blog/nx-packages-compromised

  3. https://socket.dev/blog/ongoing-supply-chain-attack-targets-crowdstrike-npm-packages 2

  4. https://www.reversinglabs.com/blog/malicious-npm-patch-delivers-reverse-shell

  5. https://socket.dev/blog/north-korean-apt-lazarus-targets-developers-with-malicious-npm-package

  6. https://socket.dev/blog/npm-registry-spam-john-wick

  7. https://github.com/duckdb/duckdb-node/security/advisories/GHSA-w62p-hx95-gf2c

  8. https://en.wikipedia.org/wiki/Npm_left-pad_incident 2

  9. https://socket.dev/blog/when-everything-becomes-too-much

  10. https://libraries.io/npm

  11. https://www.theregister.com/2016/03/29/npmgate_followup

  12. https://nodejs.org/en/learn/typescript/run-natively

  13. https://github.blog/security/supply-chain-security/our-plan-for-a-more-secure-npm-supply-chain 2

  14. https://github.com/dominictarr/event-stream/issues/116

  15. https://en.wikipedia.org/wiki/XZ_Utils_backdoor

  16. https://openssf.org/blog/2024/04/15/open-source-security-openssf-and-openjs-foundations-issue-alert-for-social-engineering-takeovers-of-open-source-projects/

  17. https://xkcd.com/2347

About

How to stay safe from NPM supply chain attacks

Topics

Resources

License

Stars

Watchers

Forks

Contributors 2

  •  
  •