Blog / Supply Chain Security

How to Mitigate npm Supply Chain Attacks: A Practical Guide for npm and Yarn

Third-party dependencies represent the largest external attack surface for modern JavaScript applications. Here is how to configure npm and Yarn to block malicious package injections.

Software supply chain security has evolved from a theoretical concern into one of the most frequent vectors for active production exploits. Because the average Node.js project pulls in hundreds of transitive dependencies, a single compromised package deep in your dependency tree can lead to data exfiltration, environment variable theft, and remote code execution. While you cannot audit every line of open-source code you import, you can configure your package managers to dramatically reduce your risk profile. This guide provides a step-by-step walkthrough of the configuration controls and commands in npm and Yarn that mitigate dependency-based supply chain attacks.

This article was created with AI assistance and reviewed by the DastPilot team.

Understanding the npm supply chain threat landscape

Before implementing defenses, it helps to understand what types of attacks you are actively trying to block:

  • Typosquatting: Attackers publish packages with names very similar to popular open-source libraries (e.g., reqeust instead of request or lodash.map instead of lodash/map) hoping developers make a spelling error.
  • Dependency Confusion: If a company uses a private package name (e.g., @company/internal-auth) but fails to register that scope or name on the public npm registry, an attacker can publish a malicious package with the exact same name on the public registry. Many default package manager configurations will pull the public version because it has a higher version number.
  • Compromised Developer Credentials: A legitimate maintainer's account is hacked, or their registry token is leaked, allowing an attacker to publish a malicious update (e.g., 1.4.2) to an otherwise trusted library.
  • Protestware / Maintainer Sabotage: A maintainer intentionally alters their package to run destructive payloads under specific conditions or in certain geographic regions.

1. Pinning dependencies with lockfiles

The first line of defense is ensuring that your local development environment and your production builds run the exact same code. By default, package.json specifies version ranges (like ^1.2.0), which allows the package manager to download newer minor or patch versions dynamically.

If an attacker compromises a package and publishes a malicious patch version, any new developer machine or build agent performing a standard install will automatically fetch the compromised code.

Mitigation: You must use lockfiles (package-lock.json for npm, or yarn.lock for Yarn) and commit them to version control. The lockfile records the exact version, resolution path, and cryptographic hash of every direct and transitive dependency.

Why integrity hashes matter

Lockfiles contain an integrity field (usually using SHA-512 hashes). During installation, the package manager checks the downloaded package's hash against the hash in the lockfile. If an attacker tampers with a package on the registry without changing the version number, the installation will fail because the integrity hashes do not match.

Lockfiles only help if you treat them as security artifacts. Review lockfile diffs in every pull request and regenerate them only from trusted developer workstations or CI runners.

2. Enforcing clean, frozen builds in CI/CD

Running a standard npm install or yarn install in your CI/CD pipeline is a major security risk. If a developer accidentally updates package.json but forgets to update the lockfile, or if a package manager dynamically resolves a different transitive dependency tree, your pipeline will run untested code.

Worse, standard installs can write updates back to your lockfile during the build, masking changes until they land in production.

In npm: Use npm ci

The npm ci (Clean Install) command is designed specifically for automated environments like testing, continuous integration, and production deployment. It differs from npm install in critical ways:

  • It requires a package-lock.json. If the lockfile is missing, it fails immediately.
  • If dependencies in the lockfile do not match those in package.json, it errors out instead of updating the lockfile.
  • It deletes the existing node_modules directory before starting, ensuring a clean state.
  • It never updates the lockfile or package.json.
# Secure CI/CD build step for npm
npm ci --only=production

In Yarn Classic (v1): Use --frozen-lockfile

Yarn v1 does not have a separate ci command, but it supports a flag that achieves the same outcome. If the installation requires changes to yarn.lock, the build fails.

# Secure CI/CD build step for Yarn Classic
yarn install --frozen-lockfile --production

In Modern Yarn (v2/v3/v4 Berry): Use --immutable

In modern versions of Yarn, the equivalent flag is --immutable. You can configure this globally or run it on the CLI:

# Secure CI/CD build step for Yarn Modern
yarn install --immutable

Reduce install-script execution where possible

Many malicious packages rely on lifecycle hooks such as preinstall, install, and postinstall to execute code during dependency installation. For CI jobs that only need to lint, validate lockfiles, or run static analysis, consider disabling install scripts entirely:

# Skip lifecycle scripts when the job does not require them
npm ci --ignore-scripts
yarn install --frozen-lockfile --ignore-scripts

Use this control selectively. Native modules and some build tooling rely on install scripts, so validate each pipeline stage before enforcing it globally.

3. Scanning for known vulnerabilities

Package managers maintain database connections to vulnerability registries. Regularly auditing your project helps you catch known vulnerable packages before they make it to production.

Using npm audit

Run npm audit to scan your dependency tree for security alerts. In CI/CD pipelines, you want the build to fail if vulnerabilities exceed a specific threshold (for example, high or critical):

# Run audit and fail only on high or critical issues
npm audit --audit-level=high

To automatically apply safe fixes (upgrading minor or patch versions), use npm audit fix. Avoid running npm audit fix --force blindly, as it can install major breaking changes that bypass your lockfile constraints.

Using yarn audit

Yarn Classic includes a built-in audit command. You can similarly set levels and output JSON for machine readability:

# Audit Yarn dependencies and fail on severe alerts
yarn audit --level high

For modern Yarn (Berry), the audit command is provided via the npm plugin. You can audit packages using:

# Yarn Modern audit command
yarn npm audit

Automated fixes can help with low-risk patch releases, but treat audit output as triage rather than a blanket remediation plan. Prioritize internet-facing services, runtime dependencies, and packages that are actually reachable in your deployed code path.

4. Registry restrictions and scoping

One of the most insidious vectors is Dependency Confusion. If your company ships an internal package called @company-name/payment-gateway but a developer machine or build agent checks the public npm registry first, an attacker who registers that same name publicly can hijack the install.

Mitigation A: Map scopes to specific registries

By using scoped packages (e.g., prefixing your packages with @mycompany/), you can explicitly bind that scope to your private registry (like JFrog Artifactory, Sonatype Nexus, or Verdaccio) in your project's local config file (.npmrc or .yarnrc.yml).

Configure this in your local .npmrc file at the root of your project:

# .npmrc Configuration
# Bind the @mycompany scope to your internal registry
@mycompany:registry=https://registry.company.com/api/npm/npm-local/
# Route all other packages to the public registry
registry=https://registry.npmjs.org/

For Yarn Modern (.yarnrc.yml):

# .yarnrc.yml Configuration
npmScopes:
  mycompany:
    npmRegistryServer: "https://registry.company.com/api/npm/npm-local/"

This configuration prevents your package manager from ever querying the public registry for packages matching your corporate scope.

Mitigation B: Use the `publishConfig` restriction

To prevent developers from accidentally publishing internal packages to the public registry, mark the package as private in its package.json, or configure publishConfig to target your internal registry:

{
  "name": "@mycompany/internal-auth",
  "version": "1.0.0",
  "private": true,
  "publishConfig": {
    "registry": "https://registry.company.com/api/npm/npm-local/"
  }
}

5. Package signature verification

In 2022, npm introduced package signatures, letting users verify that a package was signed by the registry and has not been altered since publication. Signatures link the package to its build source (like GitHub Actions) via cryptographic proofs (Sigstore).

To check signatures, npm provides CLI commands to inspect the cryptographic details of a package:

# Verify the signature and provenance of a package
npm audit signatures

This command queries the registry for signature details of all installed packages, ensuring that the signatures match the registry's public keys. If a package was injected or altered on a mirror or proxy registry, the signature verification will flag the anomaly.

npm vs. Yarn security command reference

Security Action npm Command Yarn Classic (v1) Yarn Modern (v2+)
Deterministic Lockfile Install npm ci yarn install --frozen-lockfile yarn install --immutable
Scan for Vulnerabilities npm audit yarn audit yarn npm audit
Force safe lockfile updates npm audit fix N/A (manual resolution) N/A (manual resolution)
Verify package signatures npm audit signatures N/A N/A
Restrict scope to private registry Configure .npmrc Configure .npmrc Configure .yarnrc.yml

Final takeaway

Dependency security is not a single tool. It is a layered operating model that combines deterministic builds, controlled registries, selective script execution, and routine auditing. Taken together, those controls block the majority of common supply chain attack paths.

Treat package managers as security gatekeepers rather than simple downloaders. Configure them defensively, review lockfile drift with the same scrutiny as source code, and make audit and signature checks part of your release criteria.

Coordinate your pipeline security with DastPilot

While package managers protect your dependencies, DastPilot secures your endpoints. Run automated API security testing on every deployment to ensure your runtime is as secure as your supply chain.

Start Free Trial