Version 1.1
This document aims to be an all-inclusive document explaining the security supply-chain best practices when using npm's package manager. It is one of the workstreams of the OSSF's Best Practices for Open Source Developers working group. This document provides
- an overview of the security features of npm in the context of supply-chain
- explicit recommendations
- details or links to the official documentation to achieve these recommendations. This document is intended to complement the official npm documentation, not to be an alternative.
- npm Best Practices Guide
Follow the principle of least privilege for your CI configuration.
If you run CI via GitHub Actions, a non-privileged environment is a workflow without access to
GitHub secrets and with non-write permissions defined, such as permissions: read-all
, permissions:
,
contents: none
, contents: read
. For more information about permissions, refer to the official documentation.
You may install the OpenSSF Scorecard Action to flag non-read permissions on your project.
The first step to using a dependency is to study its origin, trustworthiness and security posture. Projects like Envoy proxy have well-documented criteria a dependency must meet before it is used.
Recommendations:
-
Be aware of typosquatting attacks, when an attacker creates an official-looking package name to trick users into installing rogue packages (1, 2, 3). Although the npm registry performs scans to detect typosquatting; no system is perfect, so stay vigilant. Identify the GitHub repository of the package and assess its trustworthiness through, for example, its number of contributors, stars, etc.
-
Additionally, uppercase characters were previously allowed, which is another attack vector to be aware of. For example: JSONStream, jsonstream.
-
Note: Non-ASCII characters are no longer supported in the npm package names, so developers need not worry about homograph attacks from the public registry, whereby an attacker names their package using non-ASCII characters that render similar to an ASCII character. Note that this property is registry-dependent; you will need to verify this policy with any registry you use.
-
-
When you determine a GitHub project of interest, follow their documentation to identify the corresponding package name.
Use OpenSSF Security Scorecards to learn about the current security posture of the dependency.
-
Use deps.dev to learn about the security posture of transitive dependencies. You may list transitive dependencies by using component analysis.
-
Use npm-audit to learn about existing vulnerabilities in the dependencies of the project.
Warning: Organization verification does not exist on the npm registry, and it's "first-come, first serve." It is possible to challenge the owner of an org for squatting after the fact using the dispute process.
In npm, the package.json describes a project (name, components, dependencies, etc.). Dependencies may be defined by a package name, URL, repo, etc. Additional constraints may be defined for each dependency, such as which tags, branches, versions, or commit-ish are allowed.
Note: The manifest file does not list transitive dependencies; it only lists direct project dependencies.
In the rest of this document, we will refer to three types of projects:
-
Libraries: These are projects published on the npm registry and consumed by other projects in the form of API calls. (Their manifest file typically contains a
main
,exports
,browser
,module
, and/ortypes
entry). -
Standalone CLIs: These are projects published on the npm registry and consumed by end-users in the form of locally installed programs that are always run stand-alone via
npx
or via a global install. An example would be clipboard-cli. (Their manifest file contains abin
entry). -
Application projects: These are projects that teams collaborate on in development and deployment, such as websites and/or web applications. An example would be a React web app for a company's user-facing SaaS. (Their manifest file typically contains
"private": true
).
A reproducible installation guarantees an exact, identical copy the dependencies are used each time the package is installed. This has various benefits, including:
-
Ensuring the dependencies installed are the ones declared and reviewed via pull requests.
-
Helping quickly indentify possible compromises of your infrastructure if one of your dependencies is found to have vulnerabilities, as you will be able to promptly determine the commit range of when your repository is at risk.
-
Mitigating certain threats such as malicious dependencies. Otherwise, you might install and run a newly published (compromised) version of the dependency on a CI/CD system or developer machine, giving an attacker immediate code execution.
-
Detecting package corruptions before installation, for example, due to RAM corruption.
-
Mitigating package mutability introduced by misconfigured registries that proxy packages from public registries. Note: Versions are immutable in principle, but the registry enforces immutability.
-
Improve the accuracy of automated tools such as GitHub's security alerts.
-
Let maintainers test updates before accepting them in the default branch, e.g., via renovatebot's stabilityDays.
There are two ways to achieve a reproducible installation reliably: vendoring dependencies and pinning by hash.
Use a lockfile because it implements hash pinning using cryptographic hashes. Hash pinning informs the package manager of the expected hash for each dependency without trusting the registries. During each installation, the package manager then verifies that the hash of each dependency remains the same. Any malicious change to the dependency would be detected and rejected.
Npm provides two options to achieve hash pinning.
The package-lock.json contains all the dependencies (direct and indirect) along with a cryptographic hash of their content:
{
"name": "A",
"version": "0.1.0",
...metadata fields...
"dependencies": {
"B": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/B/-/B-0.0.1.tgz",
"integrity": "sha512-DeAdb33F+"
"dependencies": {
"C": {
"version": "git://github.com/org/C.git#5c380ae319fc4efe9e7f2d9c78b0faa588fd99b4"
}
}
}
}
}
The package-lock.json
file is a snapshot of an installation that allows
later reproduction of the same installation. As such, the lock file is
generated or updated via the various commands that install packages, e.g., npm install
. If some dependencies are missing or not pinned by hash (e.g.,
the integrity
field is not present), the installation will patch the lock file to
reflect the installation.
The lock file cannot be uploaded to a registry, which means that
consumers who locally install the package via npm install
may see different
dependency
versions
than the repo owners used during testing. Using package-lock.json
is akin to
dynamic linking for low-level programming languages: the loader will resolve
dependencies at load time using libraries available on the system. Using this
lock file leaves the task of deciding dependencies' versions to use to the
package consumer.
npm-shrinkwrap.json
is another lock file supported by npm. The main difference is that this lock
file may be uploaded to a registry along with the package. This ensures that
consumers obtain the same dependencies' hashes as the
repo owners intended. With npm-shrinkwrap.json
, the package producer takes
responsibility for updating the dependencies on behalf of their consumers. It's
akin to static linking for low-level programming languages: everything is
declared at packaging time.
To generate the npm-shrinkwrap.json
, an existing package-lock.json
must be present,
and the command npm shrinkwrap
must be
run.
Certain npm
commands treat the lockfiles as read-only, while others do not.
The following commands treat the lock file as read-only:
-
npm ci
, used to install a project and its dependencies, -
npm install-ci-test
, used to install a project and run tests.
The following commands do not treat the lock file as read-only, may fetch / install unpinned dependencies and update the lockfiles:
-
npm install
,npm i
,npm install -g
-
npm update
-
npm install-test
-
npm pkg set
andnpm pkg delete
-
npm exec
,npx
-
npm set-script
Recommendations:
-
Developers should declare and commit a manifest file for all their projects. To create the manifest file, use the official Creating a package.json file documentation.
-
To add a dependency to a manifest file, locally run
npm install --save <dep-name>
and commit the updated manifest to the repository. -
If you need to run a standalone CLI package from the registry, ensure the package is a part of the dependencies defined in your project via the
package.json
file, before being installed at build-time in your CI or otherwise automated environment. -
Developers should declare and commit a lockfile for all their projects. The reasoning is that this lockfile will provide the benefits of Reproducible installation by default for privileged environments (project developers' machines, CI, production or other environments with access to sensitive data, such as secrets, PII, write/push permission to the repository, etc).
When running tests locally, developers should use commands that treat a lockfile as read-only (see Lockfiles and commands), unless they are intentionally adding/removing a dependency.
Below we explain the lockfile acceptable by project type.
-
If a project is a library:
-
An
npm-shrinkwrap.json
should not be published. The reasoning is that version resolution should be left to the package consumer. Allow all versions from the minimum to the latest you support, e.g.,^m.n.o
to pin to a major range;~m.n.o
to pin to a minor range. Avoid versions with critical vulnerabilities as much as possible. Visit the semver calculator to help define the ranges. -
The lockfile
package-lock.json
should be ignored for tests running in CI (e.g. vianpm install --no-package-lock
). The reasoning is that CI tests should exercise a wide range of dependency versions to discover/fix problems before the library users do, so tests need to pull the latest versions of packages. -
Locally, developers should only run npm commands that treat the lockfile as read-only (see Lockfiles and commands), except when intentionally adding /removing a dependency.
-
Follow the principle of least privilege in your CI configuration. This is particularly important since the lockfile is ignored.
-
-
If a project is a standalone CLI:
-
Developers may publish an
npm-shrinkwrap.json
. Remember that by declaring annpm-shrinkwrap.json
, you take responsibility for rapidly and consistently updating all the dependencies. Your users will not be able to edit or deduplicate them. If you expect your CLI to be used by other projects and defined in theirpackage.json
or lockfile, do not usenpm-shrinkwrap.json
because it will hinder dependency resolution for your consumers: follow the recommendations as if your project was a library. -
In CI, only run npm commands that treat the lockfile as read-only (see Lockfiles and commands).
-
Locally, developers should only run npm commands that treat the lockfile as read-only (see Lockfiles and commands), except when intentionally adding /removing a dependency.
-
Follow the principle of least privilege in your CI configuration.
-
-
If a project is an application:
-
Developers should declare and commit a lockfile to their repository.
-
In CI, only run npm commands that treat the lockfile as read-only (see Lockfiles and commands).
-
Locally, developers should only run npm commands that treat the lockfile as read-only (see Lockfiles and commands), except when intentionally adding /removing a dependency.
-
Vendoring dependencies denotes keeping a local copy of all the dependencies (direct and transitive) in the repository. While vendoring can solve the "reproducible installation" problem, it also can encourage insecure practices. These include poor ability to audit dependency code, difficulties keeping dependencies up to date, and more. Also, vendoring introduces non-security problems like repository size, usability, and developer experience issues. For these reasons, vendoring is not recommended without tooling and solutions to address those problems outside this document's scope.
It is crucial to update dependencies periodically, particularly when new vulnerabilities are disclosed and patched. Tools that help with dependency management are easy to use and may implement security checks for you.
Recommendations:
-
To manage your dependencies, use a tool such as dependabot or renovatebot. These tools submit merge requests that you may review and merge into the default branch.
-
Stay up to date about existing vulnerabilities in your dependencies:
-
If you have installed the tools above, enable security alerts: see dependabot config and renovatebot config. Note that renovatebot follows config-as-code, so it makes it easier for external users to verify that it is enabled.
-
If you prefer a dedicated tool, run npm-audit periodically, e.g., in a GitHub workflow.
-
-
To remove dependencies, periodically run
npm prune
and submit a merge request. The tools above do not support this feature yet, and we are not aware of a GitHub action for this feature.
Vulnerability disclosure comes in two major halves. Researchers discovering and reporting vulnerabilities to software maintainers and software maintainers further notifying the users of their software to known vulnerabilities. The OpenSSF maintains a set of general recommendations regarding vulnerability disclosure which maintainers should consult for more details.
Software maintainers should make it easy and clear how to privately disclose vulnerabilities. GitHub recommends creating a security.md file with contact information that a researcher can use to privately disclose security vulnerabilities they discover. Maintainers should ensure that some method of private communication is possible for well-intentioned security researchers lest they accept that all disclosure be public.
Most projects tend to have vulnerabilities discovered in them over their lifetime and it's important that those vulnerabilities get disclosed to users in a clear and concise manner. Disclosing vulnerabilities with a CVE, GHSA, or other indexing number will allow automated systems to discover, ingest, and report to users any relevant vulnerabilities. For these automated systems to work well the source advisory should be as detailed as possible calling out specific npm package names, versions, and code changes which resolve the issue.
Publishing on the npm registry requires creating a user account.
Recommendations:
- Developers should enable 2FA on their account.
npm packages are all signed by a ECDSA Key owned by the default npm registry.
Signatures can be verified with the npm CLI v8.15.0 or later with the command npm audit signatures
.
Warning: npm does not support user-level signatures.
Publishing a package uploads it to a registry for others to install and download.
Recommendations:
-
When using a CI system to publish, use an Automation token to authenticate and publish to the default npm registry.
-
Release your package using the commands:
npm ci npm publish
-
Consumers of public packages may fall victim to typosquatting attempts. To mitigate this problem, and create and own your organization on other registries.
Note:
-
The default npm registry does not support the default GitHub token for authentication, so users need to save it as a GitHub secret.
-
There is no official GitHub action to publish an npm package.
-
CIDR-scoped tokens are scoped by IP range for additional protection but can only be created with the npm CLI.
Dependency confusion attacks, a.k.a substitution attacks, occur when a project relies on private packages from an internal registry. An attacker may register the same package name on public registries. By default, npm will fetch the dependency from the public registry—see details of recent attacks in Alex Birsan's blog. To alter this behavior so npm fetches the correct dependency, use scopes.
A "scope" is a @-prefixed name that goes at the start of a package name. For
example, @myorg/foo
is a scoped package. Scoped packages are used like any
other packages in package.json and Javascript code:
{
"name": "@myorg/foo",
"version": "1.2.3",
"description": "just a scoped package name example",
"dependencies": {
"@myorg/bar": "2.x"
}
}
// es modules style
import foo from "@myorg/foo";
// commonjs style
const foo = require("@myorg/foo");
Scoped packages on the public npm registry may only be published by the user or organization associated with it, and packages within that scope may be made private. Also, scope names can be linked to a given registry.
Recommendations:
-
On the public npm registry, create a free organization with the “myorg” name. At that point, no one else can publish anything under the
@myorg
scope on the public registry and your builds will fail with404 errors
if they’re misconfigured, rather than silently fetching untrusted content. This is crucial because it prevents an attacker from hijacking your organization’s name in the public registry, which could result in the same problems. -
Locally, use the login command to ensure that all requests for packages under the
@myorg
scope will be made to thehttps://registry.myorg.local
registry. Any other requests not bound to a scope will go to the default registry.npm login --scope=myorg --registry=https://registry.myorg.local
This saves the login information in your
~/.npmrc
file as follows:@myorg:registry = https://registry.myorg.local/ //registry.myorg.local:_authToken = xyzabc123-arbitrary-token-value
-
Do not commit this file
~/.npmrc
with credentials to a repository. -
In automated environments, provision the secret automatically. Follow a similar solution as GitHub uses in workflows. If your registry supports ephemeral credentials, use it instead of using long-term secrets/tokens.
-
Create a
.npmrc
file in the root of your projects, with a line like this@myorg:registry = https://registry.myorg.local/
You can check the currently configured registry at any time by running this command.
npm config get registry
By doing this, npm will associate the scope with your internal registry when working on that project.
For more information on the topic, see npm's substitution attack blog post.
If you use internal private registries:
-
Only use private registry solutions that support scopes
-
Make your private packages immutable:
-
Ensure your registry is not configured to “merge” manifests of the same name from the upstream public registry. This is sometimes enabled to work around resolution collisions, but it is a terrible idea, precisely because “workaround resolution collisions” is how name hijacking exploits work. If possible, use a private registry implementation that doesn’t even have this feature.
-
Once a package is published to the internal proxy registry, it is very important that you do not silently fall back to the public registry if that package is ever removed. If the proxy starts serving this package name from the public registry, you are back in a situation where an attacker can take over the name and gain access to any systems that are left behind.
-
-
Do not ignore build failures. If you configure your projects as above, you will likely see a 404 error rather than npm fetching untrusted content. Do not ignore these errors! Configure your systems to crash as loudly as possible if a build fails, and fix it immediately.
For more information on the topic, see npm's substitution attack blog post.