Safeguard
Open Source Security

package-lock.json integrity checks and the npm audit work...

A step-by-step guide to verifying package-lock.json integrity, running npm audit correctly, and scanning Node.js dependencies for tampering and vulnerabilities.

Priya Mehta
DevSecOps Engineer
8 min read

If your CI pipeline installs whatever npm install decides to resolve at build time, you don't actually have reproducible builds -- you have a suggestion. package-lock.json integrity is the mechanism that turns that suggestion into a guarantee: every package your project depends on is pinned to an exact version and a cryptographic hash, so a compromised registry, a hijacked maintainer account, or a tampered CDN response gets caught before malicious code ever reaches node_modules. This matters more every year, as npm supply-chain attacks increasingly target the install step itself rather than the published source. In this guide you'll learn how the integrity field actually works, how to enforce it with npm ci, how to build a real npm audit workflow around it, and how to wire both into CI so a broken or tampered lockfile fails the build instead of silently shipping.

Step 1: Understand What package-lock.json Integrity Actually Verifies

Open any package-lock.json and look at an entry:

"node_modules/lodash": {
  "version": "4.17.21",
  "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
  "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4/GHmg5UdN/xn1uKvHdBnh4jP2Kx4NcYt8AT7/CjLh0Ox+CJpO4V4l/BTfXbA=="
}

That integrity field is a Subresource Integrity (SSRI) hash -- almost always SHA-512, base64-encoded. When npm downloads the lodash tarball, it hashes the bytes it received and compares that hash against the one recorded in the lockfile. If they don't match, the install aborts. This is the core npm lockfile security guarantee: the lockfile isn't just recording "which version," it's recording "which exact bytes," which is what actually stops a tampered package from being installed transparently.

The catch is that this protection only works if you install in a mode that enforces the lockfile rather than one that's willing to update it.

Step 2: Use npm ci Instead of npm install in Every Automated Environment

npm install will happily rewrite package-lock.json if it decides a dependency's semver range allows a newer version. That's useful on a developer's laptop; it's a liability in CI, where you want byte-for-byte reproducibility and no silent drift.

npm ci

npm ci refuses to run if package.json and package-lock.json are out of sync, installs exactly what's pinned, and verifies every package's integrity hash against the lockfile as it goes. If a hash mismatch occurs -- because a package was republished, a mirror served corrupted data, or someone tampered with the tarball in transit -- the install fails loudly instead of silently succeeding with different code than what was reviewed.

Make this non-negotiable in your pipelines:

# .github/workflows/ci.yml
steps:
  - uses: actions/checkout@v4
  - uses: actions/setup-node@v4
    with:
      node-version: 20
  - run: npm ci
  - run: npm run build
  - run: npm test

Never let a CI job fall back to npm install "just to get it working" -- that fallback is exactly the gap attackers rely on.

Step 3: Run the npm Audit Workflow on Every Install

Integrity checks confirm you got the bytes the lockfile expects; they don't tell you whether those bytes contain a known vulnerability. That's what npm audit is for, and it should run as a standard part of your npm audit tutorial-style onboarding for any Node.js project:

npm audit

This compares your installed dependency tree against the npm/GitHub Advisory Database and reports known CVEs by severity (low, moderate, high, critical). For CI gating, don't just eyeball the output -- fail the build on a threshold:

npm audit --audit-level=high

This exits non-zero if any high or critical vulnerability is found, which you can wire directly into a pipeline step. Avoid running npm audit fix --force unattended in CI; it can bump major versions and break your build in ways that are harder to review than the vulnerability itself. Treat npm audit fix as a developer-driven step, run locally, with the resulting lockfile diff reviewed in a pull request like any other change.

Step 4: Verify Individual Package Integrity Manually When You Need to Be Certain

Sometimes npm audit and npm ci aren't enough -- you want to independently confirm that a specific tarball matches its recorded hash, for example during an incident investigation or before vendoring a package. You can do this with standard tools, no npm required:

curl -sL https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz -o lodash.tgz
openssl dgst -sha512 -binary lodash.tgz | openssl base64 -A

Compare the output against the base64 string after sha512- in the lockfile's integrity field. A mismatch here is a serious signal -- either the registry served you something different than what was recorded, or the lockfile entry itself was tampered with (which is why lockfiles should be code-reviewed like any other file, not treated as generated noise to ignore in diffs).

Step 5: Lock Down Lockfile Changes in Code Review

A tampered package-lock.json is one of the quieter supply-chain attack vectors because reviewers routinely skip over lockfile diffs -- they're long, machine-generated, and easy to rubber-stamp. Don't skip them. Configure your repo so lockfile changes are visible and intentional:

# .gitattributes
package-lock.json linguist-generated=false

This keeps GitHub from collapsing the diff by default. Pair it with a branch protection rule requiring at least one human approval on any PR that touches package-lock.json, and consider a CI check that fails if package-lock.json changes without a corresponding package.json change (a common indicator of an unreviewed or unexpected dependency bump):

git diff --name-only origin/main...HEAD | grep -q package-lock.json && \
git diff --name-only origin/main...HEAD | grep -q package.json || \
(echo "Lockfile changed without package.json change -- review required" && exit 1)

Step 6: Extend Beyond npm audit With Broader Node.js Dependency Scanning

npm audit only catches vulnerabilities already published to an advisory database, and only for packages resolvable through the npm registry. It won't catch novel malware in a freshly published package, typosquatted names, install-script abuse, or drift introduced by transitive dependencies your team never explicitly reviewed. A mature Node.js dependency scanning practice layers additional checks on top:

  • Enable npm audit signatures (npm 9+) to verify registry-signed package provenance where available.
  • Review and restrict lifecycle scripts (preinstall, postinstall) since they execute arbitrary code on every install -- use npm install --ignore-scripts in CI where scripts aren't required.
  • Run periodic full dependency scans against a threat-intel-backed source, not just the advisory database, to catch newly disclosed malicious packages faster than the standard feed.

Verification and Troubleshooting

"npm ci fails with 'lockfile is out of sync'." This means package.json and package-lock.json disagree -- usually because someone edited package.json by hand or merged a branch without regenerating the lockfile. Fix it locally with npm install, review the resulting diff carefully, and commit the corrected lockfile rather than bypassing the check.

"Integrity checksum failed" during install. Don't retry blindly. First check whether the package was recently republished (some registries allow unpublish/republish within a short window, which can shift hashes). If the version wasn't republished and the mismatch persists, treat it as a potential tampering event: isolate the build, check registry status pages, and verify the hash manually using the OpenSSL method from Step 4 before allowing the install to proceed.

"npm audit reports vulnerabilities with no available fix." Check whether the vulnerable package is a direct or transitive dependency with npm ls <package-name>. For transitive dependencies, overrides in package.json (npm 8.3+) lets you force a patched version without waiting on an upstream maintainer:

"overrides": {
  "vulnerable-package": "^2.1.4"
}

"CI is slow because npm audit runs on every commit." Cache the audit results keyed to the lockfile hash, and only re-run the full audit when package-lock.json changes -- there's no new information to gain from auditing an unchanged dependency tree on every push.

How Safeguard Helps

npm ci and npm audit are necessary, but they're checkpoints, not continuous monitoring -- they only tell you what's true at install time, on the machine that happens to run them. Safeguard extends package-lock.json integrity and npm lockfile security into a continuous, organization-wide control: every lockfile change across every repository is automatically diffed, hashed, and checked against known-good provenance data, so a tampered dependency or a suspicious version bump is flagged before it merges, not after it's already running in production.

On top of standard npm audit workflow coverage, Safeguard's Node.js dependency scanning correlates advisory data with real-time threat intelligence, catching newly published malicious packages and typosquats that haven't yet made it into the standard advisory feed. Findings are enriched with reachability analysis -- so your team spends review time on the vulnerabilities actually exercised by your code paths, not every CVE that happens to exist somewhere in the dependency tree -- and routed straight into the pull request, alongside the lockfile diff, so integrity verification becomes a normal part of code review rather than a separate audit that happens after the fact.

Never miss an update

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