How to Pin Hashes in requirements.txt for Python Security?
Problem
I ran pip install -r requirements.txt in production. The build succeeded. But I had no idea if the packages I downloaded were the same ones the developer tested. PyPI could have served me anything.
Here’s what a typical requirements.txt looks like:
requests==2.31.0urllib3==2.0.7certifi==2023.7.22This file pins versions but provides no integrity verification. If an attacker compromises PyPI or a package maintainer’s credentials, they can serve malicious code with the same version number.
Why this is dangerous
Supply chain attacks on PyPI have increased significantly. Attackers use several techniques:
1. Typosquatting - Upload "reqeusts" (typo of "requests") - Users accidentally install malicious package
2. Dependency confusion - Attacker registers internal package name on PyPI - pip installs public malicious version instead of private
3. Compromised maintainer - Attacker gains access to maintainer's PyPI credentials - Uploads malicious version of legitimate package - Same version number, different codeThe critical insight from a Reddit discussion on PyPI dependency hygiene:
“CVE databases lag supply chain attacks by 24-72 hours. Your fastest signal is: this package hash changed when I didn’t push anything.”
Without hash pinning, pip blindly trusts whatever PyPI serves. You need cryptographic verification.
What is hash-checking mode?
Pip has a built-in hash-checking mode. When enabled, pip verifies that every downloaded package matches a known-good cryptographic hash before installing.
1. You specify expected hash in requirements.txt2. pip downloads the package3. pip computes SHA256 hash of downloaded file4. pip compares computed hash to expected hash5. If match: install proceeds6. If mismatch: installation fails immediatelyThis protects against:
- Packages modified in transit
- Malicious packages uploaded to PyPI after compromise
- Typosquatted packages with wrong hash
How to pin hashes manually
Step 1: Get the package hash
pip download requests==2.31.0pip hash requests-2.31.0-py3-none-any.whlOutput:
--hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003fStep 2: Add hash to requirements.txt
requests==2.31.0 \ --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \ --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1Note: Multiple hashes are allowed for packages with multiple distribution formats (wheel, source tarball). Pip will accept any matching hash.
Step 3: Install with hash verification
pip install --require-hashes -r requirements.txtThe --require-hashes flag forces pip to verify hashes. If any package lacks a hash, pip refuses to install it.
I tried it and got an error
I added hashes to my direct dependencies but got this error:
ERROR: Hashes are required in --require-hashes mode, but they are missing from some requirements.urllib3 is missing hashes.certifi is missing hashes.The problem: I only pinned hashes for my direct dependencies. But requests depends on urllib3, certifi, and other packages. Pip’s hash-checking mode requires hashes for ALL dependencies, including transitive ones.
The real solution: pip-compile
Manually tracking hashes for every transitive dependency is impractical. A single package update might require updating dozens of hashes. Use pip-tools instead.
Step 1: Install pip-tools
pip install pip-toolsStep 2: Create requirements.in
Create a requirements.in file with only your direct dependencies:
requests==2.31.0flask==3.0.0Step 3: Compile with hashes
pip-compile --generate-hashes requirements.inOutput:
## This file is autogenerated by pip-compile# To update, run:## pip-compile --generate-hashes requirements.in#certifi==2023.7.22 \ --hash=sha256:539cc1d13202e33ca466e88b2807e4f3fef0d4189ded1f13787b6e0e8e9e18f9 \ --hash=sha256:539cc1d13202e33ca466e88b2807e4f3fef0d4189ded1f13787b6e0e8e9e18f9 # via requestscharset-normalizer==3.3.2 \ --hash=sha256:8c4d2d03b668a7c5c6c6c9c6c9c6c9c6c9c6c9c6c9c6c9c6c9c6c9c6c9c6c9c6 # via requestsflask==3.0.0 \ --hash=sha256:7736e6c57c3c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5 # via -r requirements.inrequests==2.31.0 \ --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \ --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1 # via -r requirements.inurllib3==2.0.7 \ --hash=sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9f55dd6f2dc91b4def \ --hash=sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e # via requestspip-compile resolves all transitive dependencies and adds hashes for every package. The # via comments show why each package is included.
Step 4: Install
pip install --require-hashes -r requirements.txtNow pip verifies every single package against its expected hash.
What happens when a hash mismatch occurs
If PyPI serves a different file than expected:
ERROR: THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS FILE. requests==2.31.0 from https://files.pythonhosted.org/packages/.../requests-2.31.0-py3-none-any.whl: Expected sha256 58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f Got a1b2c3d4e5f6... (different hash)The installation fails immediately. The malicious or corrupted package never reaches your environment.
CI/CD integration
Hash verification should be mandatory in CI. Add it to your pipeline:
name: Install Dependencies
on: [push, pull_request]
jobs: install: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.11'
- name: Install dependencies with hash verification run: pip install --require-hashes -r requirements.txt
- name: Run tests run: pytestIf any package hash changes unexpectedly, the CI fails. Vulnerable code cannot deploy.
Combining with pip-audit for defense in depth
Hash pinning protects against tampering. pip-audit protects against known vulnerabilities. Use both:
name: Security Check
on: [push, pull_request]
jobs: security: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.11'
- name: Verify package integrity (hashes) run: pip install --require-hashes -r requirements.txt
- name: Scan for known vulnerabilities (CVEs) with: inputs: requirements.txtLayer 1: Hash verification - Protects against tampering - Detects unexpected changes - Fails CI immediately on mismatch
Layer 2: CVE scanning (pip-audit) - Detects known vulnerabilities - Reports fix versions - Updates daily with new CVEsCommon mistakes
Mistake 1: Only pinning direct dependencies
requests==2.31.0 \ --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f# Missing: urllib3, certifi, charset-normalizer, idnapip-compile solves this automatically. It resolves and hashes all transitive dependencies.
Mistake 2: Forgetting —require-hashes
pip install -r requirements.txt # Ignores hashes!Without --require-hashes, pip uses hashes as hints but does not enforce them. Always use:
pip install --require-hashes -r requirements.txtMistake 3: Not updating hashes on version changes
When you want to update a package:
# Just change the version numberrequests==2.32.0 \ --hash=sha256:58cd2187... # Old hash for 2.31.0!This fails with hash mismatch. Always regenerate with pip-compile:
# Update requirements.inecho "requests==2.32.0" > requirements.in
# Regenerate requirements.txtpip-compile --generate-hashes requirements.inMistake 4: Relying solely on hashes
Hash pinning protects against tampering but not against vulnerabilities in legitimate code. A compromised maintainer could publish malicious code with the correct hash.
Threat | Hash Pinning | pip-audit-------------------------------|--------------|----------Package tampering in transit | YES | NOPackage tampering at PyPI | YES | NOKnown CVEs in packages | NO | YESMalicious code from maintainer | NO | NO (if no CVE)Combine hash pinning with:
- pip-audit for CVE scanning
- Dependency review for new packages
- Maintainer reputation checks
Scheduled hash monitoring
A Reddit comment highlighted an important practice:
“Hash the installed package metadata at deploy time and run pip-audit on a cron. CVE databases lag supply chain attacks by 24-72 hours. Your fastest signal is: this package hash changed when I didn’t push anything.”
Add monitoring to detect unexpected changes:
#!/bin/bash# /usr/local/bin/check-requirements-hash.sh
STORED_HASH="/var/cache/requirements.hash"CURRENT_HASH=$(sha256sum requirements.txt | awk '{print $1}')
if [ -f "$STORED_HASH" ]; then if [ "$CURRENT_HASH" != "$(cat $STORED_HASH)" ]; then echo "ALERT: requirements.txt hash changed unexpectedly!" # Send notification exit 1 fifi
echo "$CURRENT_HASH" > "$STORED_HASH"# /etc/cron.d/requirements-monitor0 6 * * * deploy /usr/local/bin/check-requirements-hash.shThis catches the scenario where someone modifies requirements.txt without going through proper channels.
Quick reference
# Install pip-toolspip install pip-tools
# Create requirements.in (direct dependencies only)echo "requests==2.31.0" > requirements.inecho "flask==3.0.0" >> requirements.in
# Generate requirements.txt with all hashespip-compile --generate-hashes requirements.in
# Install with hash verificationpip install --require-hashes -r requirements.txt
# When updating a package:# 1. Edit requirements.in# 2. Run pip-compile --generate-hashes requirements.in# 3. Commit both files
# Compute hash for a downloaded wheelpip download requests==2.31.0pip hash requests-2.31.0-py3-none-any.whlSummary
Hash pinning in requirements.txt provides cryptographic verification that every installed package matches known-good values. This protects against supply chain attacks where PyPI or maintainer credentials are compromised.
Key points:
- Use
--require-hashesto force hash verification - Use pip-compile to automatically generate hashes for all dependencies
- Integrate hash verification into CI/CD
- Combine with pip-audit for defense in depth
- Monitor for unexpected hash changes
The 24-72 hour lag between a supply chain attack and CVE publication means hash verification is often your fastest detection mechanism. If a package hash changes when you didn’t update requirements.txt, something is wrong.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 pip Secure Installs Documentation
- 👨💻 pip Repeatable Installs
- 👨💻 pip-audit GitHub Repository
- 👨💻 pip-tools GitHub Repository
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments