pip, uv, poetry, and pipenv allows declaring dependencies with version ranges rather than exact versions. When a project is built on a new system, the package manager performs dependency resolution: it queries the package registry and selects a specific version satisfying each declared range, then records the result in a lock file. Committing this lock file to source control ensures that all subsequent builds — on any machine or CI system — use exactly the same resolved versions.

Why is this an issue?

Without a committed lock file, dependency resolution runs anew on every fresh build, always selecting the latest version available that satisfies the declared range. This means different developers and CI systems may silently use different dependency versions, leaving the project exposed if a malicious version of a dependency is published within the allowed range.

Note, if you are authoring a library, an argument can be made to avoid lock files to allow detecting upstream dependency changes that could otherwise silently break your downstream consumers' builds. However, regularly updating your lock file ensures reproducible builds while still giving you visibility into upstream dependency changes.

What is the potential impact?

If an attacker publishes a malicious version of a dependency within your declared version range, an unlocked project may automatically adopt it on the next fresh build. This can result in arbitrary code execution, data exfiltration, or backdoors being silently introduced into your application as part of a supply chain attack.

How to fix it in pip

Installing dependencies is safe as long as a valid, up-to-date lock file is present. However, relying on this implicit behaviour means a stale or missing lock file can silently cause the package manager to resolve and download new versions, opening the door to dependency substitution attacks. Using a command that explicitly requires the lock file to exist and be respected makes the expectation clear and prevents accidental updates from slipping in unnoticed.

Code examples

Noncompliant code example

steps:
  - bash: pip install -r requirements.txt # Noncompliant

Compliant solution

Use pip install --require-hashes to install from the required lock file requirements.txt (with hashes).

steps:
  - bash: pip install --require-hashes -r requirements.txt

How to fix it in uv

Installing dependencies is safe as long as a valid, up-to-date lock file is present. However, relying on this implicit behaviour means a stale or missing lock file can silently cause the package manager to resolve and download new versions, opening the door to dependency substitution attacks. Using a command that explicitly requires the lock file to exist and be respected makes the expectation clear and prevents accidental updates from slipping in unnoticed.

Code examples

Noncompliant code example

steps:
  - bash: uv sync # Noncompliant

Compliant solution

Use uv sync --frozen to install from the required lock file uv.lock.

steps:
  - bash: uv sync --frozen

Noncompliant code example

steps:
  - bash: uv pip install -r requirements.txt # Noncompliant

Compliant solution

Use uv pip install --require-hashes to install from the required lock file requirements.txt (with hashes).

steps:
  - bash: uv pip install --require-hashes -r requirements.txt

Noncompliant code example

steps:
  - bash: uvx ruff # Noncompliant

Compliant solution

Use uvx --from with a pinned version to run a tool at a verified version.

steps:
  - bash: uvx --from ruff==0.4.0 ruff

How to fix it in Poetry

Installing dependencies is safe as long as a valid, up-to-date lock file is present. However, relying on this implicit behaviour means a stale or missing lock file can silently cause the package manager to resolve and download new versions, opening the door to dependency substitution attacks. Using a command that explicitly requires the lock file to exist and be respected makes the expectation clear and prevents accidental updates from slipping in unnoticed.

Code examples

Noncompliant code example

steps:
  - bash: poetry update # Noncompliant

Compliant solution

Use poetry install to install from the required lock file poetry.lock.

steps:
  - bash: poetry install

How to fix it in Pipenv

Installing dependencies is safe as long as a valid, up-to-date lock file is present. However, relying on this implicit behaviour means a stale or missing lock file can silently cause the package manager to resolve and download new versions, opening the door to dependency substitution attacks. Using a command that explicitly requires the lock file to exist and be respected makes the expectation clear and prevents accidental updates from slipping in unnoticed.

Code examples

Noncompliant code example

steps:
  - bash: pipenv install # Noncompliant

Compliant solution

Use pipenv sync to install from the required lock file Pipfile.lock.

steps:
  - bash: pipenv sync

Resources