Rule 2.2 — Hash-pin every Python requirement
Tier 2 · Hardened
For Python projects, every dependency entry that CI installs must be pinned to an exact version and include a SHA-256 hash. pip (and uv pip) refuse the install if any package’s content doesn’t match the hash.
Why it matters
Section titled “Why it matters”Tier 1 asked you to commit a lockfile. For Python that mostly means poetry.lock, uv.lock, or Pipfile.lock — and those tools track hashes by default. The gap Rule 2.2 closes is the very common case of a project using plain pip with a requirements.txt file: even with == pins, an attacker who replaces the artifact on PyPI (account takeover, registry mirror compromise, name-similar typosquat resolved by your private index) defeats the version pin because nothing checks what was actually downloaded.
--hash=sha256:<digest> makes the install content-addressable. pip install --require-hashes -r requirements.txt then fails if any downloaded tarball’s hash doesn’t match. The defended-against attack class is registry tampering between lock and install time — the same surface Sigstore’s npm/Maven/PyPI signing efforts are aimed at, with hashes as the immediate mechanism that works today without any registry-side cooperation.
How to do it
Section titled “How to do it”With uv (recommended for new projects)
Section titled “With uv (recommended for new projects)”uv compiles a hashed requirements.txt from a pyproject.toml (or a starter requirements.in):
uv pip compile pyproject.toml --generate-hashes -o requirements.txtEach line in the output looks like:
flask==3.1.0 \ --hash=sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136 \ --hash=sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836acTwo hashes per package — one for the wheel, one for the source distribution — because either may be selected at install time depending on the platform.
CI install:
uv pip install --require-hashes -r requirements.txt# or, with the older toolchainpip install --require-hashes -r requirements.txt--require-hashes is the gate. Without it, hashes in the file are advisory; with it, missing or mismatched hashes fail the install.
With pip-tools
Section titled “With pip-tools”pip-tools does the same thing without uv:
pip-compile --generate-hashes --output-file requirements.txt requirements.inThen in CI:
pip install --require-hashes -r requirements.txtWith Poetry
Section titled “With Poetry”Poetry’s lockfile already tracks hashes; the equivalent install gate is:
poetry install --no-update --sync--no-update refuses to modify poetry.lock; --sync removes packages not in the lockfile. Hash verification happens by default.
With Pipenv
Section titled “With Pipenv”pipenv sync honors Pipfile.lock hashes. The --system flag is appropriate in containers and CI.
Multiple requirements files
Section titled “Multiple requirements files”If you split runtime and dev dependencies (requirements.txt, requirements-dev.txt), compile each with --generate-hashes and install each with --require-hashes separately. The hashes from one file don’t carry over.
How to verify
Section titled “How to verify”-
deterministic-depsrules (see the rule catalogue):python/hash-pinned-requirement(medium) —requirements*.txtlines without--hash=are flaggedpython/lockfile-required(high) —pyproject.tomlorPipfilewithoutpoetry.lock/uv.lock/Pipfile.lock
-
Alternatives:
pip-audit --require-hasheschecks the runtime state but doesn’t enforce hash presence at commit time. Tooling likesafetyandpyupcover vulnerability scanning, not hash-pinning specifically. -
Manual:
Terminal window grep -vE '^\s*(#|--|$)' requirements.txt | grep -v -- '--hash=sha256:' \&& echo "unhashed requirements found"
Common pitfalls
Section titled “Common pitfalls”- Generating hashes once, then editing
requirements.txtby hand. Hashes go stale silently; the install will fail when CI hits the mismatch, and the diff is unreadable. Always regenerate viauv pip compile/pip-compile; never edit a hashedrequirements.txtdirectly. - Forgetting
--require-hashesin CI. Without the flag, hashes are documentation, not enforcement. - Mixing hashed and unhashed entries in the same file.
piprefuses to install with--require-hashesif any line is missing--hash=. Compile-and-commit the full file as a unit. - Pinning
pip-toolsoruvitself loosely. The tool that produces hashes is also a supply-chain risk. Pin it in a constraints file or use a pre-built image with the version pinned. - Assuming
--hash=works on git or local file references. It doesn’t — hash-pinning is for registry installs only. Git deps go via thepython/git-sharule (full commit SHA in the requirement spec); local file deps are versioned by the project’s own commit.
Real example
Section titled “Real example”No public Ozark-Security-Labs project ships a hash-pinned Python requirements.txt today; the org’s Python footprint is limited (forkguard-rulegen is the only Python project and ships via pyproject.toml without a registry-publish flow). For a strong external example, see pip-tools’ usage docs on hashes, which document pip-compile --generate-hashes as the canonical pattern. This is a known gap in the OSL example set; bringing forkguard-rulegen (or any future OSL Python project) to a hash-pinned baseline is a follow-up.