"Commit your lockfile" has been the standard npm supply-chain advice for a decade. It's necessary. It's also not sufficient. Attackers across 2025 and into 2026 have been slipping malicious packages past SCA tools by targeting the lockfile itself — not the package.json, not the registry, but the artifact everyone has been telling you to trust.
The shape of the attack
In short:
- Attacker gets a PR merged that touches the lockfile (
package-lock.json,yarn.lock, orpnpm-lock.yaml). - The PR looks innocuous: "chore: update dependencies", "fix: align lockfile with package.json".
- The diff includes a resolved URL change for one transitive dependency — pointing to a look-alike registry or a pinned tarball the attacker controls.
- CI runs
npm ci, which honors the lockfile'sresolvedURL, pulls the attacker's tarball, and builds it into the production artifact. - SCA scans the
package.jsonand sees the same top-level versions. Everything "looks clean."
The attack works because SCA tools have historically treated the lockfile as ground truth for what's installed but haven't validated that what's resolved matches the declared registry.
The primitives
Resolved-URL swap
In package-lock.json, every package has a resolved field:
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-..."
}Swap the resolved URL to https://registry.attacker.example/lodash-4.17.21.tgz. Compute an integrity hash for the malicious tarball. Commit. npm ci will install from the attacker's URL with the matching integrity.
Integrity subresource poisoning
Some lockfiles include an integrity field but not resolved. Depending on the client and config, the package manager may fall back to a different registry if the first one 404s. Attacker 404s the legitimate path and serves a matching-integrity tarball from theirs.
Lockfile drift via merge
Attacker opens a PR. CI runs npm install — not npm ci — on the PR branch to "re-resolve" the lockfile. The install pulls from the attacker's registry due to a crafted .npmrc in the PR. The regenerated lockfile now contains the attacker's resolved URLs. When the PR merges, those URLs are the new ground truth.
What SCA actually checks
Most SCA tools through mid-2025 checked:
- Top-level dependency names and versions against CVE databases.
- License compliance.
- Occasionally the installed
node_modulestree structure.
Most did not, until recently, check:
- That
resolvedURLs in the lockfile point to the declared registry. - That
integrityvalues match what the registry actually serves. - That CI hasn't regenerated the lockfile on a branch where
.npmrcwas modified.
The tools have been catching up. Not all of them. Verify yours.
What to check this quarter
- Registry URL policy. Grep your lockfiles for
resolvedfields. Every URL should match your expected registry (registry.npmjs.orgor your internal mirror). Automate this as a CI check that fails the build on drift.
- .npmrc provenance. An
.npmrcin the repo, in a workspace, or set via CI env var can redirect installs. Inventory every.npmrcthe build sees. Any PR that touches one is a high-risk diff.
- Install vs. CI. Production builds should use
npm ci(oryarn --frozen-lockfile, orpnpm install --frozen-lockfile) — never plaininstall. The bare install command is permitted to regenerate the lockfile. CI is not the place to regenerate it.
- Integrity validation on the registry side. If you use a proxy (Artifactory, Verdaccio, GitHub Packages), confirm it validates upstream integrity. Some proxies cache whatever was served; a race in the cache can make a swap persist.
- PR diff gating. Any PR whose diff touches a lockfile's
resolvedURL or integrity field should require a maintainer review plus a registry-policy CI check.
The mental-model shift
The lockfile is input to your build, not a fixed artifact. It's code. It's attacker-reachable in the same ways other code is. Treat it with the same review discipline as your Dockerfile or your CI config.
"Commit your lockfile" was good advice. "Verify your lockfile, pin your registry, and gate your CI re-resolves" is the 2026 advice.
