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:
- It reads
package-lock.json— it does not resolve ranges. - It downloads the exact package version listed.
- It computes the hash of the downloaded archive.
- It compares that hash to the value stored in the lockfile.
- 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.