Free — no signup required

Pinning Dependencies: Lockfiles and Hashes

3 min read

The Danger of "Latest"

When you write flask>=2.0 or "axios": "^0.21.0" in a dependency file, you are writing a range, not a version. You are telling your package manager: "Give me anything that satisfies this constraint." That means two builds a week apart can install different code. This is called non-deterministic builds, and it is a security and reliability problem.

If a maintainer's account is compromised and a malicious 2.0.1 is published, your next pip install will silently pull it down. You will never know unless you are actively checking.

The Solution: Lockfiles

Pinning means recording the exact version and the cryptographic hash of every package — including transitive dependencies — so that every future install is byte-for-byte identical to the one you tested.

Lockfiles by ecosystem:

Ecosystem Manifest (ranges) Lockfile (pinned)
Node.js package.json package-lock.json or yarn.lock
Python pyproject.toml / requirements.in poetry.lock / Pipfile.lock
Go go.mod go.sum
Ruby Gemfile Gemfile.lock
Rust Cargo.toml Cargo.lock

Lockfiles must be committed to version control. This is a common mistake — developers add *.lock to .gitignore because the files are large. This defeats the entire purpose.

Hash Verification in Practice

Lockfiles store a SHA-256 or SHA-512 hash of each package archive. When npm ci (note: ci, not install) runs in your pipeline:

  1. It reads package-lock.json — it does not resolve ranges.
  2. It downloads the exact package version listed.
  3. It computes the hash of the downloaded archive.
  4. It compares that hash to the value stored in the lockfile.
  5. If they differ by even a single byte, the build fails immediately.

This protects against three threat classes: compromised package registries, man-in-the-middle attacks on the download, and "silent republish" attacks where a maintainer overwrites an existing version with different content. This is exactly why Node's CI-oriented install mode exists as a distinct command from the everyday install: the everyday command still resolves ranges and can install different versions run to run, while the CI-oriented one uses the lockfile exactly and fails outright on any hash mismatch. Python's equivalent is an explicit flag that forces every install to be hash-verified against the lockfile rather than trusting the registry's response by default. A hash-pinned Python requirements entry names two hashes per package — one for the wheel format, one for the source tarball — so pip can verify whichever format it happens to download.

Senior depth — the transitive dependency problem: Pinning your direct dependencies is necessary but not sufficient. The event-stream attack was delivered through a transitive dependency — a package your direct dependency pulled in. Lockfiles solve this because they record the entire resolved tree, not just the top level. This is why the CI-oriented install mode matters: an install mode that only pins direct dependencies in the manifest leaves every transitive dependency unpinned and exactly as exposed as before.

This is one of 18 chapters

Every chapter is free — Kubernetes, Terraform, SRE, distributed systems, and more — with fast daily review built in.

Sign up free