Software Supply Chain Security

npm Lockfile Injection Attacks: How Tampered package-lock.json Files Compromise Builds

Lockfile injection is a subtle supply chain attack where malicious changes to package-lock.json redirect dependency resolution to attacker-controlled packages. Here is how it works and how to detect it.

Yukti Singhal
Security Researcher
5 min read

Most JavaScript developers understand that package-lock.json exists to make builds reproducible. Fewer understand that the lockfile itself can be a vector for supply chain attacks. Lockfile injection is a technique where an attacker modifies the lockfile to point to malicious package versions or registries, while leaving package.json looking completely normal.

The attack is effective because lockfiles are large, auto-generated, and rarely reviewed in pull requests. A careful attacker can slip in a single changed resolved URL or integrity hash among thousands of lines of lockfile changes, and most code reviewers will scroll right past it.

How npm Dependency Resolution Works

When you run npm install in a project with an existing package-lock.json, npm does not re-resolve dependencies from the registry. It trusts the lockfile. The lockfile contains the exact version, download URL (resolved), and cryptographic hash (integrity) for every installed package.

This means the lockfile determines exactly what code ends up in node_modules. If an attacker can modify the lockfile, they control what gets installed -- regardless of what package.json says.

The Attack Vector

A typical lockfile injection attack follows this pattern:

Step 1: Fork and modify. The attacker forks a popular project or submits a pull request that includes lockfile changes. The PR might add a legitimate dependency or update an existing one.

Step 2: Tamper with resolved URLs. Hidden among the legitimate lockfile changes, the attacker modifies the resolved field for one or more packages to point to a malicious registry or a republished package with backdoor code.

Step 3: Update integrity hashes. The attacker updates the integrity field to match the malicious package, so npm does not flag a checksum mismatch.

Step 4: Wait for merge. If the PR is merged without lockfile review, every subsequent npm ci or npm install will fetch the attacker's package instead of the legitimate one.

Why This Attack Is Hard to Detect

Several factors make lockfile injection particularly insidious:

Lockfile diffs are noisy. A single dependency update can change hundreds of lines in the lockfile. Reviewers develop "lockfile blindness" and approve changes without inspection.

The resolved URL format is opaque. URLs like https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz are long and similar. Swapping the registry hostname or version is easy to miss.

Automated tools do not check. Most SAST and SCA tools analyze package.json or the installed packages, not the lockfile resolution metadata itself.

CI/CD trusts the lockfile. Running npm ci in CI is a best practice for reproducible builds, but it means CI installs exactly what the lockfile specifies -- even if the lockfile has been tampered with.

Real-World Examples

In 2021, researchers demonstrated that lockfile injection could be used to redirect packages to attacker-controlled registries. The attack was particularly effective against projects that used scoped packages, where the registry URL could be changed to a private registry that the attacker controlled.

The event-stream incident in 2018, while not strictly a lockfile injection, demonstrated the same principle: attackers targeted the dependency resolution chain rather than the application code itself. The malicious code was introduced through a transitive dependency that was not visible in the project's direct dependency list.

Detecting Lockfile Injection

Validate lockfile consistency. Use npm ci instead of npm install in CI. While npm ci trusts the lockfile, it will fail if the lockfile is inconsistent with package.json. More importantly, use tools like lockfile-lint that specifically validate lockfile entries.

Enforce registry allowlists. The lockfile-lint tool can verify that all resolved URLs point to expected registries. If your project only uses the public npm registry, any resolved URL pointing elsewhere is suspicious.

Review lockfile changes in PRs. This is tedious but necessary for security-critical projects. Focus on changes to resolved URLs and integrity hashes, especially for packages that were not intentionally updated.

Use npm's built-in verification. Running npm audit signatures (available in npm 8.13+) verifies that packages were signed by the registry. This catches packages served from unofficial registries.

Implement lockfile-only PRs. When updating dependencies, create separate PRs for lockfile changes so they can be reviewed independently from code changes.

The lockfile-lint Tool

The lockfile-lint tool by Liran Tal specifically addresses lockfile security. It can:

  • Validate that all resolved URLs use HTTPS
  • Verify that all packages resolve to an allowed list of hostnames
  • Check that all packages have valid integrity hashes
  • Detect mixed registries in a single lockfile

Configuration is straightforward:

{
  "type": "npm",
  "path": "package-lock.json",
  "allowedHosts": ["npm"],
  "allowedSchemes": ["https:"],
  "allowedUrls": ["https://registry.npmjs.org"]
}

Beyond npm: Yarn and pnpm

This attack is not limited to npm. Yarn's yarn.lock and pnpm's pnpm-lock.yaml contain similar resolution metadata. The same principles apply: if an attacker can modify the lockfile, they can redirect dependency resolution.

pnpm has an advantage here because its lockfile format is more human-readable (YAML), making manual review somewhat easier. Yarn Berry (v2+) uses a different resolution strategy that includes checksums but is still vulnerable if the lockfile is tampered with.

Organizational Defenses

For teams managing multiple projects, consider these broader defenses:

Private registry proxying. Use a private registry (Artifactory, Nexus, Verdaccio) as a proxy for npm. All lockfile entries should resolve to your private registry, making unauthorized registry URLs immediately obvious.

Git hooks for lockfile validation. Add pre-commit or pre-push hooks that run lockfile-lint before lockfile changes can be committed.

Dependency update automation. Use Renovate or Dependabot for dependency updates. These tools regenerate lockfiles from package.json, which eliminates manually-introduced lockfile tampering.

SBOM generation from lockfiles. Generate SBOMs from your lockfiles and compare them against expected component lists. Unexpected packages or registries in the SBOM indicate potential tampering.

How Safeguard.sh Helps

Safeguard.sh provides continuous monitoring of your dependency chain, including lockfile integrity validation. The platform tracks all packages resolved in your builds and flags unexpected registry sources, version changes, or integrity mismatches. When combined with SBOM generation and policy enforcement, Safeguard.sh creates a comprehensive defense against lockfile injection and other dependency resolution attacks, ensuring that what you specify in package.json is exactly what ends up in your production builds.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.