FOSSA Logo

Dependency Pinning

Dependency pinning is the practice of locking software dependencies to specific versions to ensure build reproducibility, stability, and security in the software supply chain.

What is Dependency Pinning?

Dependency pinning is the practice of explicitly specifying exact versions of software dependencies rather than using version ranges or latest version references. This technique locks dependencies to specific, verified versions to ensure build reproducibility, prevent unexpected changes, and protect against supply chain attacks.

Unlike flexible version constraints (such as ^1.2.3 or >=2.0.0), pinned dependencies use exact version specifications (like 1.2.3 exactly) or cryptographic checksums to guarantee that the same code is used in every build.

Why Dependency Pinning Matters

Supply Chain Security

When dependencies aren't pinned, builds can automatically incorporate new versions that may introduce:

  • Intentional malicious code: Supply chain attacks where attackers publish malicious updates
  • Unintentional vulnerabilities: New security flaws in otherwise legitimate updates
  • Breaking changes: Functionality changes that impact application behavior

Pinning dependencies creates a stable foundation where each component has been vetted before use.

Build Reproducibility

Unpinned dependencies lead to "works on my machine" problems when different environments pull different versions. Pinning ensures:

  • Consistent builds across development, testing, and production
  • Ability to recreate builds months or years later
  • Reliable debugging by eliminating version differences as variables

Change Management

Dependency pinning transforms dependency updates from automatic, unpredictable events to explicit, controlled changes:

  • Updates become intentional decisions rather than side effects
  • Each update can be individually tested and verified
  • Changes are documented in version control with clear ownership

Common Dependency Pinning Techniques

Various strategies exist for pinning dependencies, each with different security and usability tradeoffs:

1. Version Pinning

The most basic approach specifies exact versions in package manifest files:

NPM (package.json):

{
  "dependencies": {
    "express": "4.17.1",
    "lodash": "4.17.21"
  }
}

Python (requirements.txt):

requests==2.27.1
flask==2.0.1

2. Lockfile Utilization

Most modern package ecosystems use lockfiles that record exact versions of both direct and transitive dependencies:

  • package-lock.json or yarn.lock for JavaScript/Node.js
  • Pipfile.lock for Python
  • Cargo.lock for Rust
  • go.sum for Go
  • Gemfile.lock for Ruby

Lockfiles should be committed to version control and used consistently across environments.

3. Checksums and Integrity Verification

Beyond version numbers, checksums verify that dependency content matches expectations:

  • Subresource Integrity (SRI) for web resources
  • go.sum includes cryptographic hashes
  • yarn.lock and package-lock.json include integrity hashes
  • Custom checksum verification in build scripts

4. Vendoring

Vendoring takes pinning further by copying dependency code directly into the project repository:

  • Guarantees availability even if the original source disappears
  • Ensures bit-for-bit identical code in all environments
  • Allows for local patches when needed
  • Increases repository size but eliminates external dependencies

5. Centralized Artifact Repositories

Organizations can maintain internal mirrors of dependencies:

  • Artifactory, Nexus, or GitHub Packages for enterprise environments
  • Verified dependencies are published internally
  • Projects pull only from trusted internal sources
  • Provides additional control layer beyond version pinning

Challenges of Dependency Pinning

While essential for security, dependency pinning introduces challenges:

1. Update Management

Pinned dependencies don't automatically receive updates, requiring:

  • Systematic processes to identify available updates
  • Security scanning to prioritize security-related updates
  • Automated tools to propose version bumps (Dependabot, Renovate)

2. Transitive Dependency Complexity

Direct dependencies have their own dependencies, creating complex trees:

  • Changes to one dependency can require rebuilding the entire tree
  • Conflicts between transitive dependencies require resolution
  • Complete pinning requires addressing the entire dependency graph

3. Developer Experience

Strict pinning can impact developer workflows:

  • More frequent version update commits
  • Potential resistance to frequent lockfile changes
  • Learning curve for proper lockfile usage

Best Practices for Dependency Pinning

1. Lockfile Discipline

  • Always commit lockfiles to version control
  • Ensure CI systems use lockfiles rather than installing fresh
  • Treat lockfile changes as significant in code reviews
  • Don't manually edit lockfiles; use proper package manager commands

2. Automated Update Workflows

  • Implement automated dependency update tools
  • Configure regular update schedules with manageable batch sizes
  • Automatically run tests against proposed updates
  • Group updates by risk level or impact

3. Verification and Vetting

  • Scan dependencies for vulnerabilities before pinning new versions
  • Review significant dependency changes for potential impact
  • Consider a holding period for major dependency updates
  • Verify checksums and signatures when available

4. Documentation

  • Document why specific versions are chosen, especially for long-term pins
  • Note known issues or vulnerabilities that affect version choices
  • Document any patches or modifications to dependencies
  • Keep records of security reviews for critical dependencies