Open Source Security

Poetry and Python Supply Chain Security

Poetry's lockfile is an asset. Its dependency resolver is a tradeoff. Here is how to run Poetry safely in a world of typosquats, dependency confusion, and unmaintained installers.

Nayan Dey
Senior Security Engineer
6 min read

Poetry has become the default dependency manager for new Python projects, which is a good thing. It produces deterministic lockfiles, handles virtual environments, and treats pyproject.toml as authoritative. But "default" does not mean "safe by default," and the Poetry-specific failure modes are worth understanding if you ship Python to production.

This piece is specifically about supply chain security with Poetry. Not general Python security, not Poetry's usability — just what goes wrong and what to do about it.

The Lockfile Is Your Security Boundary

poetry.lock pins exact versions and includes hashes. That is the single most important thing about it. An attacker who compromises PyPI after your lockfile was generated cannot trick your poetry install into pulling a malicious version, because the hash will not match and Poetry will abort.

This only works if:

First, you commit poetry.lock to the repository. I have seen teams gitignore it because "it causes merge conflicts." That team was effectively running pip install with no pinning. Commit the lockfile.

Second, you use poetry install --sync or poetry install without --no-verify or similar hash-skipping flags. The defaults are safe; do not go looking for flags that turn off hash verification.

Third, you regenerate the lockfile in a controlled way. poetry lock without any constraint changes should be a no-op. If a scheduled job regenerates your lockfile weekly and nothing else changed, Poetry will pull in any upstream version bumps that match your declared ranges. That is usually what you want, but it means the review for that PR should include a diff of the lockfile, not just a rubber stamp.

Are Your Source Priorities Correct?

Poetry 1.5 introduced explicit source priorities (primary, supplemental, explicit). Before that, the behavior when a package existed on both PyPI and a private index was unclear, and dependency confusion attacks were plausible.

Dependency confusion, for context: an attacker publishes a package with the same name as your internal private package on public PyPI, at a higher version number. Your package manager prefers the higher version and pulls the malicious one. Alex Birsan's 2021 writeup demonstrated this against Apple, Microsoft, and dozens of other companies.

With Poetry, the defense is explicit. Declare your private index with priority = "explicit" and your internal packages with a source = "my-private-index" qualifier, so Poetry only looks at the private index for those specific packages. If you leave it as priority = "supplemental" or the legacy default, Poetry may still prefer a higher-version public package, which is exactly the attack.

Audit your [tool.poetry.source] sections. If you have a private index and it is not priority explicit, change it.

Group Separation Is Security

Poetry 1.2 introduced dependency groups. Use them. Your [tool.poetry.group.dev.dependencies] should include pytest, black, mypy. Your [tool.poetry.group.security.dependencies] might include pip-audit or linting tools. Your main [tool.poetry.dependencies] should be only what runs in production.

This matters because poetry install --only main in your production Docker build will skip the dev group entirely, reducing the production attack surface. I have seen production images that shipped with Jupyter, pytest, and a full static analysis toolchain because the team only used one dependency section. Every extra package is a potential CVE.

Which Installer Is Running?

Poetry ships with its own dependency resolver and installer, but under the hood it eventually shells out to pip or uses the installer library. Poetry itself has had CVEs — GHSA-37jv-xp8j-m9r2 was a 2023 issue around git URL handling that could enable code execution during poetry install if the pyproject.toml specified a malicious git URL.

This is why poetry install on untrusted repositories (for example, as part of CI against a pull request from an external contributor) is not a safe operation. Treat it like running untrusted code, because it effectively is. Sandbox it.

Poetry's Own Dependencies

poetry self has its own world. The poetry binary is a Python application with its own dependency tree, pinned separately from your projects. Running an old Poetry is a separate decision from running old project dependencies. CVE-2022-24439 in GitPython affected Poetry indirectly via its use of GitPython for git source handling.

Update Poetry itself on a cadence. poetry self update is the path, or reinstalling via pipx if you installed it that way. I recommend pinning a Poetry version in CI (via the POETRY_VERSION environment variable or the install-poetry.py script's --version flag) so different machines do not resolve differently. Reproducibility is a security property.

Should You Trust poetry.lock Across Platforms?

Poetry's lockfile includes platform-specific information. A lockfile generated on a macOS machine will include macOS-specific wheel hashes. When the same lockfile is used on Linux in CI, Poetry will fetch the Linux wheels instead — but this means the lockfile is validating a superset of what any single machine installs.

For most projects this is fine. For high-security environments, you may want to generate per-platform lockfiles or use poetry-plugin-export to export requirements.txt with hashes for the specific target platform, then pip install --require-hashes. This is a tighter boundary.

Private Package Authentication

Poetry's handling of credentials for private indexes has historically been a rough edge. poetry config http-basic.my-source user password stores the password, but where depends on your system's keyring availability. If keyring is broken or unavailable, Poetry falls back to plain text in a config file. I have seen CI logs with credentials visible because the keyring was not installed in the container.

Use POETRY_HTTP_BASIC_<source>_USERNAME and POETRY_HTTP_BASIC_<source>_PASSWORD environment variables in CI, scoped via secret management. Never commit an auth.toml.

Upgrading Is a Distinct Project

poetry update respects your version ranges and updates within them. poetry add foo@latest moves the range itself. These are different operations and you need both: routine poetry update for minor version bumps within declared ranges, periodic poetry add package@latest to consciously adopt new major versions.

A security update to cryptography or urllib3 should arrive via poetry update cryptography and produce a small, reviewable diff. A major version bump of Django should be a separate, deliberate PR with a human making the compatibility assessment.

How Safeguard Helps

Safeguard ingests poetry.lock directly and generates a Python SBOM with resolved versions and hashes, so you can query "which services use package X version Y" in a single pass rather than grepping lockfiles across repos. Reachability analysis tells you whether a CVE in a transitive dependency actually reaches production code paths or sits in a dev-only import tree. Griffin AI reads your pyproject.toml groups and source priorities to flag dependency confusion risk configurations before they reach production. Policy gates block deploys when the lockfile diverges from the committed version or when a newly introduced package fails supply chain provenance checks.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.