Join our Newsletter — 33% off our NHI Course
Home› Guides› Dependency Pinning vs Version Ranges: Choosing Between Supply…
Guide Software & AI Supply Chain

Dependency Pinning vs Version Ranges: Choosing Between Supply Chain Safety and Patch Speed

← All guides
By NHI Mgmt Group, edited by Lalit Choda Updated 14 November 2026 18 min read
On this page

A version range lets every fresh install pull whatever a maintainer, or an attacker holding the maintainer's token, published a few minutes ago, while an exact pin freezes the code you tested and also freezes the vulnerabilities in it. The argument is usually framed as pin or float, but the useful question is where each decision is made: in the manifest, in the lockfile, in the install command, or in the update bot. This guide compares exact pinning, hash pinning and semantic version ranges as security controls: the threat each one addresses, how each one fails, how lockfiles and hashes change the picture, and how to combine them with cooldowns and update automation so you get reproducible builds without slow patching. It is for application security engineers, build teams and library maintainers. Bot configuration itself is covered in the Dependency Update Automation Guide; this page does not repeat it.

Key takeaways

  • Applications and libraries need different answers. An application should resolve to exact, hash-verified versions at build time; a library should declare the honest range it works with, because only its manifest, not its lockfile, reaches consumers.
  • The lockfile is the real pin. A caret range in package.json is harmless if CI installs strictly from a committed lockfile with integrity hashes; an exact pin in the manifest is weak if the lockfile is missing or ignored.
  • Hash pinning is what stops tampering. A version number names a release; a hash identifies the bytes. pip hash-checking mode, npm integrity fields and Go's go.sum all refuse content that does not match what was recorded.
  • Pinning without updating is a vulnerability policy. OpenSSF Scorecard itself warns that pinning can inhibit updates, so every pin needs an owner and an update path with a defined remediation time frame.
  • A cooldown is the bridge between the two. Pin exactly, let a bot propose new versions only after they have aged a few days, and fast-track advisory fixes. Most malicious releases are removed within hours.

The short answer

For anything you deploy (services, containers, CLI tools, CI workflows), resolve every direct and transitive dependency to an exact version and a content hash, commit that record, install only from it in CI, and move pins forward through reviewed pull requests from an update bot with a release-age delay. For a library others consume, publish a version range that reflects real compatibility, never a wildcard, and keep a committed lockfile only for your own reproducible test runs. Treat third-party CI actions and container base images as dependencies that must be pinned by commit SHA or digest. The rest of this page explains why, and where each option breaks.

Pinning, ranges and lockfiles side by side

Three distinct controls hide behind the word "pinning". The table separates them from ranges so the comparison is like for like.

DimensionVersion range in manifestExact version pin in manifestLockfile or hash pin (resolved record)
What it fixesAn allowed set, for example npm ^1.2.3 meaning >=1.2.3 <2.0.0-0One version string per direct dependencyExact version and content hash for every direct and transitive dependency
Transitive dependenciesFloat within their own rangesStill float unless a lockfile existsFixed
Protects against a malicious new releaseNo: a fresh resolve can select it immediatelyPartly: direct dependency fixed, transitive ones notYes, until someone regenerates the lock
Protects against a tampered artefact at the same versionNoNo: same version string, different bytes still installsYes, when hashes are checked (integrity, --hash, go.sum)
Patch latencyLow for new installs; existing builds unchanged until reinstalledHigh unless a bot raises updatesHigh unless a bot regenerates the lock
ReproducibilityPoor: two builds a day apart can differPartialHigh
Consumer impact (libraries)Lets consumers deduplicate and patch transitive versionsForces duplicate copies or resolution conflicts downstreamNone: lockfiles are not used by consumers
Main failure modeSilent adoption of a compromised or breaking releaseStale, vulnerable versions and false confidence about transitive codeLock drift, regenerated without review, or ignored by the install command

How each option fails: threat model

Version ranges

The attacker's goal is to get code into your build without touching your repository. A range is a standing invitation: any release inside it is accepted on the next resolve. The common route is a stolen maintainer or publishing token used to push a new patch release, which every caret and tilde consumer is eligible to receive. In the Nx s1ngularity attack, a token taken through a GitHub Actions workflow was used to publish infected versions that were live for about four to five hours and ran a post-install script on every machine and pipeline that installed them. Ranges decide whether your next install during that window picks it up. Self-propagating worms such as the one described in the Shai-Hulud npm Worm Guide multiply this: each stolen token publishes more poisoned patch releases into more ranges.

Ranges also carry quieter risks. Semantic version promises are made by humans and broken by accident, so a minor release can change behaviour. In npm semantics a caret on a 0.x version is narrower than many developers assume (^0.2.3 allows only <0.3.0-0, and ^0.0.3 allows nothing beyond 0.0.3), which leads teams to loosen ranges to * or latest to get updates, removing every limit.

Exact pins without hashes

An exact version in the manifest stops a new release of that direct dependency from being adopted, which is why many teams believe they are protected. Two gaps remain. First, the dependency's own dependencies still resolve from ranges unless a lockfile exists, and a poisoned release of a transitive dependency is adopted just as easily as one of a direct dependency. Second, a version string is a name, not content: if a registry, mirror or proxy serves different bytes under the same version, a pin without a hash installs them. The longer-term failure is staleness: pins that nobody owns age into known-vulnerable versions, and OWASP ASVS 5.0 requirement V15.2.1 (components within documented remediation time frames) is failed quietly.

Lockfiles and hash pins

A lockfile with hashes closes both gaps, but it fails in operational ways. It can be regenerated wholesale by a developer running an update command, pulling in hundreds of changes in one unreviewed diff. It can be ignored if CI uses a command that rewrites it rather than one that enforces it. It can be bypassed for private modules, as Go documents for modules matched by GOPRIVATE or GONOSUMDB, or when GOSUMDB=off. And it does nothing if the first resolution already captured a malicious version: the hash then faithfully protects the attacker's bytes.

How the main ecosystems implement pinning

Each package manager puts the controls in different places. The table lists what the official documentation describes, so you can tell which file is the real pin in your stack.

EcosystemRange syntax lives inResolved recordContent verificationStrict install for CI
Go modulesgo.mod (minimum versions)None needed: minimal version selection is deterministic and "the build list is not saved in a 'lock' file"go.sum SHA-256 (h1:) hashes, checked against the transparent checksum database at sum.golang.org (GOSUMDB)A hash mismatch is reported as a security error and the download is deleted
npmpackage.json (^, ~, ranges, tags, URLs)package-lock.json (lockfileVersion 3 for npm v9 and later); npm-shrinkwrap.json if publishedintegrity field: Subresource Integrity string, typically sha512npm ci: requires a lockfile, exits with an error if it does not match package.json, never writes either file
Python (pip)requirements files and project metadata (>=, ~=, ==)A fully pinned requirements file, or a tool lockfile--hash=sha256:... per requirement; hash-checking mode--require-hashes: every requirement, including dependencies, must be pinned with == and hashed or the install fails
Rust (Cargo)Cargo.toml version requirementsCargo.lock, tracked by default by cargo newChecksums recorded in Cargo.lock--locked style flags fail if the lock would change

Three details matter for security reviews. Go's minimal version selection never picks up a new upstream release until something requires it. npm's lockfile cannot be published, so a library's lockfile never protects its consumers; npm-shrinkwrap.json can be published but npm advises it mainly for CLI tools and deployed packages. And pip ignores index-supplied hashes in download URLs for --require-hashes: the docs explain those guard against corruption, not tampering, so the hashes must come from your own resolved record.

The same logic applies outside language package managers. GitHub's hardening guidance tells users to pin third-party actions to a full-length commit SHA because "a tag can be moved or deleted if a bad actor gains access to the repository", and now describes workflow dependency lockfiles as superseding SHA pinning for repositories that adopt them. OpenSSF Scorecard's Pinned-Dependencies check asks for Dockerfile base images and GitHub workflow dependencies to be pinned by hash rather than by a mutable tag or version range. See the GitHub and GitHub Actions Security Guide and the Container Image Security Guide.

Architecture: where each decision should live

The disagreement between "pin everything" and "use ranges" mostly disappears once the decision points are separated. A sound design has four layers, each with one job.

  1. Manifest (intent): declares the versions the code is compatible with. Libraries use honest ranges (Cargo's FAQ: libraries "should always specify the range that they do work with", never *). Applications may use ranges or exact versions; it matters less because the next layer decides.
  2. Lockfile (decision): records the exact resolution and content hashes for the whole tree. It is committed, code-reviewed like source, and changed only by a deliberate update.
  3. Install (enforcement): CI and production builds install only from the lockfile in strict mode (npm ci, pip install --require-hashes, Go's go.sum verification, Cargo's locked mode), and fail rather than resolve fresh versions.
  4. Update (change): a bot or scheduled job proposes lockfile changes as small pull requests, after a release-age cooldown, with tests and review, and with a separate fast path for security advisories.

A registry proxy can add a fifth layer: a release-age rule and block list applied to every fetch. The Package Registry Security Comparison Guide covers registry-side controls, and the Malicious Package and Dependency Confusion Guide covers resolution order between internal and public sources, which pinning alone does not fix.

Patch speed: measuring the real cost of pinning

The case against pinning is patch latency: a pinned tree only moves when someone changes it. The question is whether the delay is controlled.

ModelWhen the fix reaches productionWhen a malicious release would reach productionWho decides
Ranges, no lockfileNext build after the fix is published, if it is inside the rangeNext build after publication: minutesNobody: the resolver
Lockfile, no update automationWhen a developer notices and regenerates: often weeks or monthsWhen a developer regenerates during the malicious windowWhoever runs the update command
Lockfile plus bot with cooldown and security fast pathAdvisory-driven PR within hours to days, merged under the remediation SLAOnly after the cooldown, by which point most malicious releases have been removedPolicy, reviewed per change

The third row is the target: faster than the second for security fixes and safer than the first for new releases, at the cost of reviewing a steady flow of update pull requests. Cooldown values per update class are in the bot guide linked above.

Controls per option

If you keep ranges in manifests

  • No wildcards or dist-tags: ban *, empty strings and latest in dependency declarations; a linter or policy check in CI can enforce it.
  • Commit the lockfile and enforce it: ranges are acceptable only when the build installs strictly from the committed lock.
  • Override known-bad transitive versions: npm overrides in the root package.json can force a specific version anywhere in the tree while upstream catches up.
  • Disable install scripts where possible: most malicious releases execute at install time; see the npm Install-Time Security Guide.

If you pin exactly

  • Pin by hash, not only by version: generate hashes for every requirement and run installs in hash-checking mode.
  • Give every pin an update path: an update bot covering every ecosystem in the repository, including CI actions and base images. Scorecard's Dependency-Update-Tool check notes it only confirms a tool is enabled, not that its pull requests are merged, so measure merges.
  • Record why a pin is held back: a pin kept below the latest version needs an issue reference and an expiry date, or it becomes permanent.

For lockfiles and hash pins

  • Review lockfile diffs: require code-owner review for lockfile changes and flag new package names, new install scripts and changed resolved URLs or registries.
  • Fail on drift: strict install commands in every pipeline, plus a check that the lockfile is consistent with the manifest.
  • Keep checksum verification on: do not set GOSUMDB=off; scope GOPRIVATE to your own module paths only.
  • Verify provenance at the first pin: hashes protect what you first captured, so check publisher provenance when a new package or version enters the lock. See the npm Trusted Publishing and Provenance Guide and the SLSA and Build Provenance Guide.

Decision guidance

SituationRecommended approachWhy
Deployed service or containerLockfile with hashes, strict install, bot with cooldownReproducibility and tamper resistance outweigh resolver convenience
Published libraryHonest ranges in the manifest; lockfile for your own CI onlyConsumers resolve from your manifest; exact pins create duplicates and conflicts downstream
CLI tool installed by usersConsider a published lock (for npm, npm-shrinkwrap.json) or a single bundled artefactUsers get the tree you tested
CI workflows and build toolsPin actions by full commit SHA or workflow lockfile; base images by digestThese run with pipeline secrets and tags can be moved
Internal packages you publishRanges or exact versions with no cooldownYou control the publisher; delay adds nothing
Regulated or high-assurance systemsHash pins, internal mirror with release-age rule, human approval for every lock changeChange control (NIST SP 800-53 CM-3) and integrity (SI-7) evidence

Small teams: commit lockfiles, switch CI to strict install, and enable an update bot with its default cooldown. That alone delivers most of the benefit. Large organisations: publish shared bot presets, run a registry proxy that enforces release age and block lists for every build, report per-team lock freshness, and treat the update bot as a privileged non-human identity with scoped, short-lived credentials.

Worked scenario: a payments service and a shared client library

A platform team owns a Node.js payments service and publishes an internal client library used by 40 other services. Before the change, the service declares caret ranges, has a lockfile, but its pipeline runs npm install, which can update the lock. The library pins every dependency exactly to "be safe".

  1. Exposure found: two builds a week apart shipped different transitive versions, and the library's exact pins forced three copies of one HTTP client into consumers, one with a known advisory.
  2. Service fix: CI moves to npm ci; lockfile changes require code-owner review; the update bot raises grouped patch and minor updates after a cooldown and security fixes immediately.
  3. Library fix: exact pins are replaced with honest caret ranges that the test suite covers at both the lowest and highest allowed versions, so consumers can deduplicate and patch transitive versions themselves.
  4. CI fix: third-party actions are pinned to full commit SHAs and the base image to a digest, with the bot updating both.
  5. Result: when a compromised patch release of a logging dependency appears and is removed by the registry within hours, the service never sees it (cooldown and strict install), while the next advisory fix in the HTTP client reaches production within the remediation SLA through a single reviewed pull request.

Implementation roadmap

  1. Inventory: list repositories by ecosystem, whether a lockfile is committed, which install command CI uses, and whether actions and base images are pinned. Scorecard's Pinned-Dependencies check gives a quick first pass for workflows, Dockerfiles and scripts.
  2. Enforce lockfiles in audit mode: add a CI job that reports when the install would change the lock, without failing builds, for two weeks.
  3. Switch to strict install: change pipelines to npm ci, hash-checking pip installs, locked Cargo builds; keep Go checksum verification on. Fail on drift.
  4. Add hashes where missing: generate hashed requirement files for Python services and confirm npm lockfiles carry integrity fields.
  5. Turn on update automation with cooldowns: follow the bot guide; start with one pilot ring of well-tested repositories.
  6. Pin CI and images: convert action tags to commit SHAs or adopt workflow lockfiles; pin base images by digest.
  7. Roll back: if a lockfile update breaks production, revert the lockfile commit and redeploy; the previous resolved tree and hashes return exactly. If the update was malicious, follow the Malicious Open Source Package Response Playbook and rotate any secrets reachable from affected builds.

Detection and measurement

  • Unlocked builds: pipeline logs showing npm install rather than npm ci, pip installs without hash-checking, or GOSUMDB=off.
  • Lockfile changes outside the bot: human-authored lockfile commits with large diffs, new registries in resolved URLs, or new install scripts.
  • Mutable references: actions referenced by tag or branch, and images referenced by tag, in any workflow or Dockerfile.
  • Yanked or flagged versions in locks: alert when a locked version is removed or flagged malicious.

Metrics that show the approach is working:

  • Percentage of deployable repositories with a committed lockfile and strict install in CI.
  • Percentage of dependencies, actions and images pinned by hash, SHA or digest.
  • Median and 90th percentile time from advisory publication to merged fix, against the remediation time frames ASVS V15.1.1 asks you to document.
  • Median age of locked versions behind latest, per team, as a staleness indicator.
  • Count of versions adopted inside the cooldown window, which should be zero except for security fixes.

Framework mapping

FrameworkReferenceHow pinning and ranges relate
NIST SP 800-53 Rev. 5CM-2 Baseline Configuration, CM-3 Configuration Change ControlThe lockfile is the dependency baseline; lock changes go through change control
NIST SP 800-53 Rev. 5SI-7 Software, Firmware, and Information Integrity; SI-7(6) Cryptographic ProtectionHash verification of every artefact at install
NIST SP 800-53 Rev. 5SI-2 Flaw Remediation; SR-3 Supply Chain Controls and Processes; SR-4 ProvenanceUpdate path for pins; supplier and provenance checks when versions enter the lock
NIST CSF 2.0PR.PS-02, ID.RA-01Software maintained commensurate with risk; vulnerabilities in pinned components identified
OWASP ASVS 5.0V15.1.1, V15.2.1, V15.2.4Remediation time frames; components within them; dependencies from the expected repository
MITRE ATT&CKT1195.001 Compromise Software Dependencies and Development ToolsThe technique that ranges expose and hash pins plus cooldowns reduce

Common failure patterns

  • Lockfile committed, never enforced: CI runs a command that re-resolves, so the lock records history rather than controlling builds.
  • Exact pins in a library: consumers inherit duplicate and stale copies they cannot patch without forking.
  • Pin and forget: no bot, no owner, no expiry; the tree ages into known-vulnerable versions while dashboards show "100% pinned".
  • Wholesale lock regeneration: a developer runs a full update to fix one package and a reviewer approves a 3,000-line lockfile diff without reading it; any malicious version published that morning comes along.
  • Tags treated as pins: CI actions and base images referenced by mutable tag while application code is hash-pinned, leaving the pipeline, which holds the secrets, as the weakest link.
  • Checksum verification switched off for convenience: GOSUMDB=off or broad GONOSUMDB patterns set once to fix a private module error and never narrowed.

Practitioner checklist

  • Commit a lockfile with content hashes for every deployable project.
  • Use strict install commands in CI and fail on lockfile drift.
  • Ban wildcards and dist-tags in manifests; libraries declare honest ranges.
  • Run pip in hash-checking mode with hashes from your own resolved record.
  • Keep Go checksum database verification on; scope GOPRIVATE narrowly.
  • Pin CI actions by full commit SHA or workflow lockfile and base images by digest.
  • Run an update bot with a release-age cooldown and a fast path for advisories.
  • Require code-owner review of lockfile diffs and flag new packages and install scripts.
  • Track time to remediate and the age of locked versions.

Standards and references

Related NHI Mgmt Group resources: Dependency Update Automation Guide · Software Supply Chain Security Guide · Open Source Security Guide · Reproducible Builds Guide

Free weekly newsletter

Subscribe to the NHI & AI Identity Journal

The latest on NHI and Agentic AI security – articles, research, breaches, news and events every week.

Bonus 33% off our NHI Course when you subscribe.

NHIMG Editorial Note
Researched and written by the NHI Mgmt Group editorial team using AI tools, under the editorial direction of Lalit Choda. Facts are drawn from the sources listed on this page and checked by our automated quality process. Last updated 14 November 2026. Spotted an error? Use our contact page.
NHI Mgmt Group, the independent authority on Non-Human Identity, IAM and Agentic AI security. nhimg.org