Join our Newsletter — 33% off our NHI Course
Home› Guides› Python Packaging and pip Security Guide: Hash Checking,…
Guide Application Security

Python Packaging and pip Security Guide: Hash Checking, Constraints and Index Configuration

PDF for newsletter subscribers · free to read online

← All guides
By NHI Mgmt Group, edited by Lalit Choda Updated 27 October 2026 13 min read
On this page

By default pip checks nothing against tampering, picks the highest version it can find across every index you give it, and may run arbitrary build code from a source distribution during install. The pip documentation says this plainly, and it explains why one stray --extra-index-url line or an unpinned transitive dependency is enough for a malicious package to reach a build server. The fixes are built into the tools: hash-checking mode, binary-only installs, constraints files, index pinning and lockfiles. This guide shows how to configure pip, pip-tools, uv and Poetry so that Python installs are repeatable and resistant to dependency confusion, how to audit them, and how to roll the controls out without breaking every pipeline. It is for Python developers, platform teams and application security engineers, and it goes deeper on installation than the Python Security Guide.

Key takeaways

  • Hash-checking mode is all or nothing. Once one requirement carries a --hash, pip requires pinned, hashed entries for every requirement and dependency, which is exactly what makes it a strong control.
  • pip has no index priority. --index-url, --extra-index-url and --find-links are searched together and the best version wins, so private package names fetched with --extra-index-url are exposed to dependency confusion.
  • uv and Poetry can pin packages to an index. uv's default first-index strategy and explicit indexes, and Poetry's explicit sources, stop a public package from shadowing an internal one.
  • Source distributions execute code. Installing an sdist runs its build backend, so --only-binary :all: removes a whole class of install-time execution.
  • Publishing is the other half. Trusted Publishing issues PyPI tokens that expire in 15 minutes, but only if leftover long-lived API tokens are revoked, as the Ultralytics compromise showed.

Why pip installs are risky by default

The pip "Secure installs" page states that, by default, pip does not perform any checks to protect against remote tampering and involves running arbitrary code from distributions. Three default behaviours drive most incidents:

  • Loose resolution: a requirement such as requests>=2 resolves to whatever is newest at install time, and its transitive dependencies are not pinned at all unless you pin them.
  • Flat index search: the pip install reference says there is no priority among the locations searched; all are checked and the best match by version number is selected. pip's own documentation warns that using --extra-index-url to find private packages is unsafe because an attacker can publish the same name with a higher version on a public index.
  • Build-time execution: an sdist is built locally, which runs code from the package (for example setup.py) with the installer's privileges. On a CI runner those privileges usually include cloud credentials and publishing tokens (ATT&CK T1195.001 Compromise Software Dependencies and Development Tools).

PyPI does serve a hash in each download URL fragment, which pip checks against corruption, but because it comes from the same remote source it does not protect against tampering and does not satisfy --require-hashes. The Malicious Package and Dependency Confusion Guide covers attacker techniques; this guide stays on the installer configuration that defeats them.

Hash checking and repeatable installs

The pip documentation describes increasingly strict levels of repeatability. Choose the level by environment:

LevelHowProtects againstDoes not protect against
Pinned versionspip freeze output or pip-compile, installed with --no-depsSurprise upgrades and new malicious versionsA compromised index or CA chain, or an index that lets files change without a new version
Hash-checking mode--hash=sha256:... per requirement, enforced with --require-hashesTampering at the index, mirror, proxy or TLS layer; file substitutionA malicious version you pinned and hashed yourself
Hashes plus binary onlyAdd --only-binary :all:Install-time build code execution from sdistsMalicious code that runs on import
Wheelhousepip wheel into a bundle, install with --no-index --no-depsIndex outages; network access during deployPlatform portability (wheels are OS and architecture specific)

Rules of hash-checking mode

  • All requirements hashed: any unhashed entry is an error, because a partly hashed file lets bad code in through the gaps.
  • All dependencies listed: a dependency not spelled out and hashed in the file is also an error, so the requirements file must be a full lock.
  • Exact pins only: requirements must use ==, a URL or a file path.
  • Strong algorithms: sha256 is recommended; md5, sha1 and sha224 are refused for --hash.
  • Multiple hashes: list one hash per distribution you might install, for example wheels for Linux, macOS and Windows plus the sdist if you allow it.
  • Escape hatch: pip 26.2 added --no-require-hashes, which verifies hashes only where present so hashed requirements can be mixed with local directories or VCS URLs. Treat its use in CI as a finding.

Generate the file rather than writing hashes by hand: pip-compile --generate-hashes requirements.in (pip-tools) or uv pip compile --generate-hashes. Install your own project with pip install --no-deps ., never python setup.py install or develop, which the pip documentation warns will download unchecked anything you missed.

Constraints files

A constraints file (pip install -c constraints.txt) controls which version of a package is installed if it is installed, without causing it to be installed. Constraints cannot be editable or carry extras. They are the right tool for an organisation-wide "never below this patched version" list or a known-bad version block applied across many projects, layered on each project's own lock. They are not a lock: they do not hash and do not pin packages they do not mention.

Index configuration and dependency confusion

Every tool resolves the private-plus-public index problem differently, and the defaults matter more than any policy document.

ToolDefault multi-index behaviourSafe configuration
pipNo priority: all indexes and find-links searched, best version winsOne --index-url pointing at an internal proxy that serves both private packages and vetted public ones. No --extra-index-url for private names.
uvfirst-index: for each package, only versions from the first index that has it are considered, which uv documents as a dependency confusion defenceKeep first-index; mark private indexes explicit = true and pin packages via [tool.uv.sources]. Treat unsafe-best-match (closest to pip) and unsafe-first-match as exceptions.
PoetryImplicit PyPI unless a primary source is configured; supplemental sources searched only if higher-priority sources yield nothingUse priority = "explicit" for private sources and a source constraint per private package. Poetry's documentation warns that a package published to a primary source can replace one from a supplemental source.

Where index settings hide

  • pip configuration files: pip loads global files such as /etc/pip.conf and /etc/xdg/pip/pip.conf, a user file such as $HOME/.config/pip/pip.conf and a site file at $VIRTUAL_ENV/pip.conf. Run pip config debug on build images to see what is actually loaded.
  • Environment variables: pip options can be set as environment variables (for example PIP_EXTRA_INDEX_URL), which often arrive from CI templates nobody reviews.
  • Requirements files: an --extra-index-url line inside a requirements file applies to the whole install.
  • Credentials: do not embed index tokens in URLs in committed files. uv reads UV_INDEX_<NAME>_USERNAME and _PASSWORD environment variables and never writes credentials to uv.lock. Index tokens are non-human credentials with read access to your private code; scope and rotate them. See the Private Package Repository and Proxy Guide.
  • Name reservation: register your internal package names, or a clearly owned prefix, on PyPI where policy allows, so they cannot be claimed by someone else.

Lockfiles in uv and Poetry

  • uv: uv.lock records resolved versions and distribution hashes. In CI use uv sync --locked, which fails if the lockfile is out of date, or uv lock --check as a separate gate. --frozen uses the lockfile without checking it against project metadata, which is faster but will not catch a pyproject.toml change that was not re-locked. For the pip interface, uv pip install --require-hashes enforces hashes, and --require-build-hashes covers build dependencies.
  • Cooling-off windows: uv's --exclude-newer limits resolution to distributions uploaded before a timestamp, which gives the ecosystem time to detect and remove malicious releases before you adopt them.
  • Malware checks: uv has a preview feature (UV_MALWARE_CHECK=1) that checks the lockfile against OSV malicious package advisories during sync. Preview features may change, so do not rely on it as your only control.
  • Poetry: commit poetry.lock, install from it in CI, and fail the job if the lock and pyproject.toml disagree.
  • Build dependencies: lockfiles cover runtime dependencies; build backends such as setuptools are fetched separately unless you pin and hash them through build constraints.

Choosing an approach

  • Small team, public packages only: pip-tools or uv with hashed locks and --only-binary :all: where your dependencies ship wheels. No proxy needed; hash checking is, in pip's words, a labour-saving alternative to a private index of approved packages.
  • Private packages: an internal proxy as the single index, or uv explicit indexes with source pinning. Never --extra-index-url.
  • Regulated or large estates: internal proxy with quarantine for new public versions, organisation constraints file, hashed locks in every repository, SBOM generation from the lock, and pip-audit in CI. See the SBOM Guide.
  • Data science and notebooks: the hardest case, because ad hoc pip install in notebooks bypasses locks. Provide curated base images and route notebook installs through the same proxy.

Phased rollout

  1. Inventory: find every install path: Dockerfiles, CI jobs, Makefiles, notebooks. Grep for --extra-index-url, --trusted-host, setup.py install and unpinned requirements. Run pip config debug on build images.
  2. Remove dependency confusion first: replace extra indexes with a single proxy index or uv explicit indexes. This is the highest-risk gap and does not require hashing yet.
  3. Generate hashed locks: introduce pip-compile --generate-hashes or uv lock per project, with a bot that refreshes them. See the Dependency Update Automation Guide.
  4. Enforce in CI: add --require-hashes (or uv sync --locked) in a non-blocking job for a sprint, fix failures caused by platform-specific wheels by adding hashes, then make it blocking.
  5. Tighten: add --only-binary :all: project by project, with a documented exception list for packages that only ship sdists; build those once into an internal wheelhouse.
  6. Audit continuously: run pip-audit -r requirements.txt --require-hashes --strict; it exits 1 when vulnerabilities are found and the exit code cannot be suppressed, so record accepted risks with --ignore-vuln ID and a review date.
  7. Rollback: if a hashed lock blocks an urgent fix, regenerate the lock with the patched version rather than disabling hash checking; keep the previous lock in version control for a one-commit revert.

Publishing securely

Consumers' controls fail if the publisher is compromised. PyPI's Trusted Publishing uses OpenID Connect: a CI system such as GitHub Actions presents a short-lived identity token, PyPI matches it against the publisher configured for the project, and mints a project-scoped API token valid for 15 minutes. Long-lived tokens remain usable until someone notices and revokes them. The Ultralytics PyPI compromise in December 2024 shows both halves: two malicious releases came out of the project's own poisoned release workflow, and two more were uploaded with a long-lived API token that was still valid alongside Trusted Publishing. Revoke static tokens once Trusted Publishing works, protect the release environment, and keep untrusted pull request input out of release workflows. The PyPI API Tokens and Trusted Publishing Security Guide covers this in depth.

Detection, metrics and framework mapping

What to monitor

  • Hash mismatch errors: pip hash mismatch failures in CI are either a missing platform hash or tampering; triage each, never bypass by deleting hashes.
  • New index URLs: alert on pull requests that add --extra-index-url, --index-strategy unsafe-best-match, --trusted-host or new [[tool.uv.index]] entries.
  • Proxy logs: requests for internal package names against the public upstream, and first-seen public packages, are early signs of confusion or typosquatting. See the Slopsquatting and Package Hallucination Guide.
  • Egress from builds: outbound connections from install steps to anything other than the proxy suggest a malicious build script (T1059.006 Python).

Metrics

  • Percentage of repositories installing with hash enforcement or a locked sync in CI.
  • Number of build configurations using --extra-index-url or unsafe index strategies (target zero).
  • Percentage of dependencies installed as wheels versus sdists.
  • Median age of the lockfile and days to adopt a patched version after a pip-audit finding.
  • Count of PyPI projects still holding long-lived API tokens after Trusted Publishing is enabled.

Framework mapping

ControlNIST SP 800-53 Rev 5NIST CSF 2.0OWASP ASVS 5.0 / ATT&CK
Hash-checking mode, locked syncsSI-7 Software, Firmware, and Information Integrity; SR-11 Component AuthenticityPR.PS-06Counters T1195.001
Single trusted index, explicit sourcesSR-3 Supply Chain Controls and ProcessesGV.SC-05V15.2.4 (components from the expected repository)
Lockfile as inventory and SBOM sourceCM-8 System Component InventoryID.RA-01V15.1.2
pip-audit in CIRA-5 Vulnerability Monitoring and Scanning, SI-2 Flaw RemediationID.RA-01V15.1.1, V15.2.1
Trusted Publishing, provenanceSR-4 ProvenancePR.PS-06Counters T1195.002 Compromise Software Supply Chain

Common failure patterns

  • Private packages through --extra-index-url: the documented dependency confusion setup, often inherited from a CI template. Replace it with a single proxy index or uv explicit indexes.
  • Hashes for top-level packages only: hand-edited files that hash direct dependencies fail with an error, and teams respond by removing all hashes. Generate full locks with pip-compile or uv instead.
  • Switching uv to unsafe-best-match to fix a resolution error: it reproduces pip's flat search and reopens confusion. Pin the package to its index instead.
  • Using --frozen everywhere: CI never notices that pyproject.toml changed without a re-lock, so reviewers approve a dependency that the build does not install, or the reverse. Use --locked in CI.
  • Installing sdists on credential-rich runners: build scripts run with every secret in the job. Use --only-binary :all: and keep publishing secrets out of install jobs. The Malicious Open Source Package Response Playbook covers response when one gets through.
  • Leftover PyPI API tokens: adopting Trusted Publishing without revoking the old token leaves a long-lived publish path open, as in the Ultralytics case.

Practitioner checklist

  • Every production install uses a generated, fully hashed lock (--require-hashes, uv sync --locked or poetry.lock).
  • No build uses --extra-index-url for private packages; uv runs with first-index and explicit private indexes, Poetry with explicit sources.
  • pip config debug and CI environment variables have been reviewed on every build image.
  • --only-binary :all: is enforced, with a documented sdist exception list built into an internal wheelhouse.
  • Build dependencies are pinned and hashed through build constraints.
  • An organisation constraints file blocks known-bad versions across projects.
  • pip-audit runs on every build with --strict, and ignored vulnerabilities have an owner and review date.
  • Index credentials come from the CI secret store, are scoped read-only and are never in committed URLs.
  • PyPI projects publish through Trusted Publishing from a protected environment, with all long-lived API tokens revoked.

Standards and references

Related NHI Mgmt Group resources: Python Security Guide · Malicious Package and Dependency Confusion Guide · PyPI API Tokens and Trusted Publishing Security Guide · Private Package Repository and Proxy Guide · Software Supply Chain Security Guide

PDF for subscribers

Take this guide with you

Every section, table and checklist in one PDF, with links to the sources and related guides. Subscribe to our weekly NHI & AI Identity Journal and we’ll email you a download link.

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 27 October 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

PDF Downloads for Newsletter Subscribers

Get the guide

Enter your details and we’ll email you a download link.

The full guide is free to read online. The PDF is a thank you for subscribing.

Fill in all fields marked * and tick the PDF box to continue.