Supply-Chain Security
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the Python Reference bibliography. |
Every pip install and poetry install runs or installs code written by strangers. This page collects the controls that pip and
Poetry offer against the common attacks: a malicious package that wins a name collision, a tampered file, a compromised new
release, and a known vulnerability nobody noticed. It was written against pip 26.2.2 and Poetry 2.5.1, and the examples were run on
CPython 3.14.6 against PyPI on 2026-10-22. The threat model behind each control is described on PyPI’s
common attacks page.
Dependency confusion
An organisation publishes birdtally-internal on its private index. A developer installs it with
python -m pip install --extra-index-url https://pypi.internal.example/simple birdtally-internal
pip now searches both PyPI and the private index and picks the best version it finds anywhere. If an attacker uploads
birdtally-internal 99.0 to PyPI, that version wins. pip’s documentation carries an explicit warning on exactly this example:
using --extra-index-url to find packages that are not on the main index "is unsafe", "a class of security issue known as
dependency confusion". There is no index priority in pip: --index-url and --extra-index-url are equal sources.
PEP 708, which would have let an index declare "this name is only valid from me" (tracks and alternate locations), was Rejected, so the fix has to come from configuration:
-
With pip, point
--index-url(PIP_INDEX_URL) at a single index that serves both your private packages and a proxy of PyPI (devpi, Nexus, Artifactory, CodeArtifact, Azure Artifacts and the GitLab registry can all do this), and do not use--extra-index-url. See pip: Package Indexes & Finding Packages. -
Reserve your internal names on PyPI if your policy allows it, so that nobody else can register them.
-
Use hash-checking mode (below): a file from the wrong index has a different hash.
Source: pip install: --extra-index-url, pip install: examples (the dependency confusion warning), PEP 708.
Poetry: source priorities and source constraints
Poetry, unlike pip, has priorities. Every [[tool.poetry.source]] is primary (the default), supplemental or explicit:
-
primary sources are all searched for every dependency without a source constraint, and configuring any primary source disables the implicit PyPI source;
-
supplemental sources are searched only if no higher-priority source has a compatible distribution, which brings back the dependency confusion problem: the Poetry documentation warns that someone could publish a matching package to a primary source;
-
explicit sources are searched only for packages that name them.
A source constraint pins a package to one source, whatever its priority:
poetry source add --priority=explicit internal https://pypi.internal.example/simple/
poetry add --source internal birdtally-internal
[tool.poetry.dependencies]
birdtally-internal = { source = "internal" }
[[tool.poetry.source]]
name = "internal"
url = "https://pypi.internal.example/simple/"
priority = "explicit"
Source constraints are not inherited: if birdtally-internal depends on another internal package, constrain that one too
(poetry add --source internal other-internal@*, possibly in a dedicated group). The Poetry documentation "strongly suggests"
source constraints for every package that only one source provides. The full priority model is on
Poetry: Repositories & Package Sources.
Hashes and binary-only installs
Hash-checking mode makes pip refuse any downloaded file whose sha256 is not listed for that requirement. It turns on as soon as
one requirement in the file carries a --hash, or with --require-hashes (PIP_REQUIRE_HASHES); since 26.2,
--no-require-hashes turns the automatic switch off. It is all-or-nothing: every requirement must be pinned with == and hashed,
including indirect ones, so that an unhashed dependency cannot sneak in. poetry install checks the hashes recorded in
poetry.lock the same way, and pylock.toml always carries hashes.
--only-binary :all: (PIP_ONLY_BINARY) adds a second layer: it forbids sdists, so no setup.py or build backend of a dependency
runs on your machine at install time. Combine both in CI and container builds:
python -m pip install --require-hashes --only-binary :all: --no-deps -r requirements.lock
The tampered-hash error and the details are on pip: Repeatable & Secure Installs.
Source: pip: Hash-checking mode.
Cooldowns: ignore releases that are too new
Malicious releases (a stolen maintainer token, a compromised CI) are usually found and removed within hours or days. A cooldown
refuses to consider any release younger than a chosen age, so that you never install one in that window. Both tools read the
upload-time that PyPI publishes in its JSON simple API.
pip: --uploaded-prior-to
--uploaded-prior-to (PIP_UPLOADED_PRIOR_TO) was added in pip 26.0 with an ISO 8601 date or time; pip 26.1 added durations in
PnD form (P3D = uploaded at least three days ago, a day being 24 hours). P0D on the command line cancels a duration set in
configuration. pip 26.2.2 was released on 2026-10-20, two days before these examples ran:
python -m pip install --dry-run --ignore-installed --no-deps --uploaded-prior-to=P3D pip
Would install pip-26.2.1
Asking for the new release explicitly shows that the filter hides it:
python -m pip install --dry-run --ignore-installed --no-deps --uploaded-prior-to=P3D "pip==26.2.2"
ERROR: Ignored the following yanked versions: 20.0, 20.3.2, 21.2
ERROR: Could not find a version that satisfies the requirement pip==26.2.2 (from versions: 0.2, 0.2.1, … 26.1.2, 26.2, 26.2.1)
ERROR: No matching distribution found for pip==26.2.2
A fixed date instead freezes the index at a point in time, which is useful for reproducing an old build:
python -m pip install --dry-run --no-deps --uploaded-prior-to=2026-04-01T00:00:00Z rich
Would install rich-14.3.3
The filter applies only to remote indexes: local files, --find-links directories and VCS URLs are always allowed. An index that
does not publish upload times makes pip fail immediately rather than silently skip the filter. Since 26.2 the option is also
respected by pip list --outdated and works with -r pylock.toml.
Poetry: solver.min-release-age
Poetry 2.4.0 added three settings: solver.min-release-age (an integer number of days, POETRY_SOLVER_MIN_RELEASE_AGE),
solver.min-release-age-exclude (package names that skip the filter) and solver.min-release-age-exclude-source (source names or
URLs that skip it). A version is considered only if all its files are older than the limit. In a throw-away project that depends
on pip:
POETRY_SOLVER_MIN_RELEASE_AGE=3 poetry lock
grep -A1 'name = "pip"' poetry.lock
POETRY_SOLVER_MIN_RELEASE_AGE=3 POETRY_SOLVER_MIN_RELEASE_AGE_EXCLUDE=pip poetry lock --regenerate
grep -A1 'name = "pip"' poetry.lock
Writing lock file
name = "pip"
version = "26.2.1"
Writing lock file
name = "pip"
version = "26.2.2"
Unlike pip, Poetry does not filter a source that publishes no upload times; it simply cannot enforce the age there. Use
-exclude-source for your own private index, where releases are trusted and often needed immediately.
Source: Poetry: solver.min-release-age, Poetry: solver.min-release-age-exclude, Poetry: solver.min-release-age-exclude-source.
A cooldown delays security fixes as much as attacks. pip’s documentation recommends pairing it with a vulnerability scanner so that you are told about a vulnerable pinned version independently of the update schedule.
Auditing known vulnerabilities
pip and Poetry do not check vulnerability databases themselves. pip-audit (third party, from the PyPA) checks an environment, a requirements file or a project against the PyPI JSON API’s advisory data or OSV:
python -m pip install pip-audit # in a tools environment, not the project
printf 'pygments==2.15.0\n' > old.txt
pip-audit -r old.txt
Found 3 known vulnerabilities in 1 package
Name Version ID Fix Versions
-------- ------- --------------- ------------
pygments 2.15.0 PYSEC-2023-117 2.15.1
pygments 2.15.0 PYSEC-2026-2987 2.20.0
pygments 2.15.0 PYSEC-2026-2987 2.20.0
The exit code is 1 when anything is found, which makes it a CI gate. On the birdtally lock from the previous page it printed
No known vulnerabilities found. For a Poetry project, export first (poetry export -f requirements.txt) and audit the export.
Source: pip-audit (third party).
SBOMs
A software bill of materials lists the components inside a distribution. PEP 770 (Final, 2025) reserved
.dist-info/sboms/ in wheels and installed projects for SBOM files in a standard format (CycloneDX or SPDX), mainly for wheels that
vendor or statically link other software, which a scanner would otherwise not see. Build backends add them on request; installers
copy them. pip itself is an example of a project that vendors its dependencies: since 26.2 it ships a CycloneDX SBOM of them at
pip/_vendor/bom.cdx.json.
Source: PyPA: The sboms subdirectory, PEP 770, CycloneDX.
Verifying attestations
Files uploaded through Trusted Publishing usually carry PEP 740 attestations: Sigstore signatures that bind the file to the CI
workflow and repository that built it. PyPI exposes them through its Integrity API and the provenance key of the JSON simple
API. Neither pip nor Poetry verifies them at install time yet; the pypi-attestations tool does, for one file at a time:
python -m pip install pypi-attestations==0.0.30 # in a tools environment
export WHEEL_DIRECT_URL=https://files.pythonhosted.org/packages/d7/73/c16e5f3f0d37c60947e70865c255a58dc408780a6474de0523afd0ec553a/sampleproject-4.0.0-py3-none-any.whl
pypi-attestations verify pypi --repository https://github.com/pypa/sampleproject $WHEEL_DIRECT_URL
pypi-attestations verify pypi --repository https://github.com/example/not-sampleproject $WHEEL_DIRECT_URL
OK: sampleproject-4.0.0-py3-none-any.whl
Verification failed: provenance was signed by repository "pypa/sampleproject", expected "example/not-sampleproject"
The second call exits with code 1. How attestations are produced is on Trusted Publishing & Attestations.
Source: PyPI: Consuming attestations.
Keep the tools themselves patched
Your installer runs with your permissions and parses untrusted archives, so its own security releases matter. The ones relevant to the baseline releases of this section:
| Tool | Fixed in | Issue |
|---|---|---|
pip |
26.2 |
CVE-2026-13346: the URL path was decoded twice when determining a link’s file name. 26.2 also rejects tar archives that write
outside the target directory through symlinks on Pythons without PEP 706, and rejects path packages in a remote |
Poetry |
2.3.3 (2026-03-29) |
A path traversal in the wheel installer that let a malicious wheel write outside the installation directory. |
Poetry |
2.3.4 (2026-04-12) |
A path traversal in sdist extraction on Python 3.10.0–3.10.12 and 3.11.0–3.11.4 that let a malicious tarball write outside the target directory. |
Poetry |
2.4.2 (2026-08-29) |
A path traversal when downloading files from a compromised URL or package source, a further sdist-extraction path traversal, and installing an artifact not listed in the lock file when the source provides no hash for it. |
pypa/gh-action-pypi-publish |
see advisories |
GHSA-vxmw-7h4f-hqxh and GHSA-5xjx-3w67-w9x9; use the current |
cibuildwheel |
4.3.2–4.3.4 |
Certificate-authority handling fixes in the bundled tooling; use 4.3.4. |
Update pip with python -m pip install -U pip in each venv, and Poetry with poetry self update (or pipx upgrade poetry). Only
the latest pip is supported.
Source: pip 26.2 changelog, Poetry history.
pip and Poetry side by side
| Control | pip | Poetry |
|---|---|---|
Only one source for a package |
Single |
Source constraint |
Verify files |
|
Always, from |
No build code at install |
|
|
Cooldown |
|
|
Audit |
pip-audit |
pip-audit on |