python
Python dependency security in 2026 — pip-audit, lockfiles, and the PyPI attacks we keep seeing
Supply-chain attacks on PyPI are routine now. Here is the toolchain we run, the lockfile discipline we enforce, and the alerts we actually act on.
14 de mayo de 2026 · 10 min · por Sudhanshu K.
Python dependency security in 2026 — pip-audit, lockfiles, and the PyPI attacks we keep seeing
The Python supply chain is under sustained attack. Not in the speculative sense — in the "another typosquatted package appeared on PyPI yesterday and three of our customers' pipelines tried to install it" sense. The 2022 PyTorch dependency confusion incident, the steady drip of malicious packages mimicking requests, urllib3, and boto3, the credential-stealing payloads hidden in packages with one-character name differences from popular libraries — this is the steady state now, not the exception.
What changes for us, as the team running managed Python for customers, is that we cannot treat dependency security as a once-per-quarter audit. It has to be a continuous, layered discipline. This post covers the tooling, the policies, and the alerts that actually catch things.
The four classes of supply chain attack
Useful to name them, because the defences differ:
- Typosquatting. A malicious package with a name one character off from a popular one.
python-requests,requestss,requesst. Catches developers who fat-finger apip install. - Dependency confusion. A malicious package with the same name as a private internal package, published to public PyPI. If a build system is misconfigured to prefer public over private indexes, the malicious version gets pulled in.
- Compromised maintainer accounts. A legitimate package's maintainer account is taken over (credential stuffing, phishing), and a new "release" is pushed containing malware. This is the most dangerous category because the package name is trusted.
- Compromised build infrastructure. The maintainer is fine, the package source is fine, but the CI pipeline that produces the wheels has been compromised. Sigstore and PEP 740 attestations partially address this, but uptake is still uneven.
A serious dependency hygiene programme defends against all four. The tooling below is the layered approach we run.
Layer 1: pip-audit, integrated into CI
pip-audit is the OpenSSF-maintained tool that checks installed (or about-to-be-installed) packages against the PyPI advisory database and OSV. It is the baseline.
pip install pip-audit
pip-audit --requirement requirements.txt --strictWhat --strict does: exits non-zero on any vulnerability, not just high-severity ones. We run it that way in CI on every PR. The noise is real — pip-audit will sometimes flag a vulnerability in a transitive dependency that the application does not actually use the vulnerable code path of. We handle this with an explicit ignore file:
pip-audit --requirement requirements.txt \
--ignore-vuln GHSA-xxxx-yyyy-zzzz \
--strictEach ignored vulnerability gets a comment in the ignore file explaining why it is safe (with a date), and a calendar reminder to revisit in 90 days. "Ignored vulnerabilities" with no expiry are how security debt accumulates.
The CI hook for every Python project we manage:
# .github/workflows/security.yml
name: dependency-security
on: [push, pull_request, schedule]
schedule:
- cron: "0 6 * * *" # daily, even when nobody pushes
jobs:
pip-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install pip-audit
- run: pip-audit --requirement requirements.lock --strictThe scheduled daily run is critical. A vulnerability disclosed today affects code that was committed and merged yesterday; you need continuous re-scanning, not just per-commit.
Layer 2: a second opinion (Safety, Snyk)
pip-audit is excellent but not omniscient. We run a second scanner on top, because the advisory databases do not have perfect overlap.
Safety (formerly safety-db, now safety-cli) maintains its own vulnerability database, partly curated commercially. It catches a non-zero number of issues that pip-audit misses, mostly recently-disclosed Python-ecosystem-specific issues.
Snyk is the heavyweight option. Worth the licence cost for customers in regulated industries because it offers fixed-version recommendations, dependency trees showing why a vulnerable package is included, and SBOM generation in CycloneDX format. We run it in the cybersecurity services tier by default.
The point of running two scanners is not duplication; it is that their databases differ. The overlap is high but the union is what you want.
Layer 3: lockfile discipline
A lockfile is a file that pins every direct and transitive dependency to a specific version, with a hash. Without a lockfile, pip install -r requirements.txt against requests>=2.0 is non-deterministic — the version you got yesterday and the version you get today may differ, and the build hash on disk certainly does.
Our standard for Python projects in 2026:
- New projects use
uvwithuv.lock— fast resolver, hash-pinned, modern tooling. The standard going forward. - Existing projects use
pip-toolswithrequirements.lockgenerated bypip-compile. - Poetry's
poetry.lockis fine and we leave it where it exists.
What is not fine:
- A bare
requirements.txtwith no hashes and no transitive pinning. - "We just
pip installwhatever is in the wheelhouse." - Dockerfiles that
pip install -Uat build time.
The hash pinning matters as much as the version pinning. With hashes in the lockfile:
requests==2.32.3 \
--hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \
--hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6
pip install --require-hashes -r requirements.lock will fail loudly if a package on PyPI has been replaced with one that does not match the expected hash. This is the single best defence against compromised-maintainer attacks — a malicious release published last week to a package you locked last month cannot be installed.
Layer 4: index pinning and dependency confusion
The dependency confusion attack works because pip will check multiple package indexes and prefer the highest version it finds. If you have a private package mycompany-internal-utils version 1.4.0 on your internal index, and an attacker publishes mycompany-internal-utils version 9.9.9 to public PyPI, an unconfigured pip will pull the public one.
The fix is to be explicit:
# pip.conf
[global]
index-url = https://pypi.internal.company.com/simple/
extra-index-url = https://pypi.org/simple/This is wrong — pip treats extra-index-url as equivalent to index-url, and the attack works.
The right configuration:
[global]
index-url = https://pypi.org/simple/
[install]
# Only this prefix is allowed from the private index
trusted-host = pypi.internal.company.comWith private packages installed only via:
pip install --index-url https://pypi.internal.company.com/simple/ \
mycompany-internal-utilsOr, better, use a single proxying index (Artifactory, AWS CodeArtifact, GCP Artifact Registry) that pulls public packages and serves private ones from the same URL, with private package names blacklisted from the public-pull path. AWS CodeArtifact's "package origin controls" are the cleanest implementation we have used.
Layer 5: SBOM and provenance
For customers with compliance requirements (anything touching ISO 27001, SOC 2 Type II, or industry-specific frameworks), we generate an SBOM at build time:
pip install cyclonedx-bom
cyclonedx-py requirements -i requirements.lock -o sbom.cdx.jsonThe SBOM goes into the artefact registry alongside the container image. When CVE-2027-XXXXX drops next year against cryptography==45.0.1, we can query every customer's SBOMs and answer "which production services contain this package" in a single query, rather than re-running scanners against every image.
PEP 740 (PyPI attestations) is rolling out: packages can ship Sigstore-signed attestations of their build provenance. We do not yet require attestations because adoption is partial, but we log the attestation status of every package we install. When attestation coverage reaches critical mass, requiring them will be a one-line change.
What gets flagged, what gets ignored
The unglamorous truth of dependency scanning is that the noise:signal ratio is bad. A typical large Python application will have 20+ "vulnerabilities" reported on any given day, almost all of them low-severity, transitive, or not in a code path the application uses.
Our triage rules:
- CVSS 9.0+ or RCE-class: investigate today, mitigate this week.
- CVSS 7.0-8.9 with a known exploit path: investigate this week, mitigate this month.
- CVSS 4.0-6.9 or transitive-only: ignore with an explicit expiry of 90 days.
- CVSS <4.0: ignore with no expiry, but re-evaluate on the next major version bump.
This is heretical to security-tool vendors who would prefer everything be a P0. It is realistic for engineering teams who have a finite amount of attention. The discipline is in the expiry — ignored mediums get re-examined every quarter, ignored lows get re-examined when the package version changes.
What we ship by default
For every Python service we run under managed operations:
- pip-audit in CI on every PR, daily scheduled run, with an explicit ignore file under version control.
- A second-opinion scanner (Safety in baseline, Snyk in the cybersecurity tier).
- Hash-pinned lockfiles enforced — builds with
--require-hashesfail if anyone slips an unpinned dependency in. - A proxying package index (CodeArtifact, Artifact Registry, or self-hosted devpi) with explicit allow-lists for private package names.
- CycloneDX SBOM generated at every container build, retained alongside the image.
- Monthly review of the ignore file with the customer security lead; everything older than 90 days is re-evaluated.
The PyPI ecosystem is far more dangerous than it was five years ago, and the trajectory is steeper, not flatter. None of these layers alone is sufficient; together they make the cost of compromising a deployment via a malicious dependency high enough that attackers move on to easier targets. Reach out if you would like us to audit your current Python supply-chain posture.
Sudhanshu K. is a Security engineer at EdgeServers (RemotIQ Pty Ltd, ABN 91 682 628 128). She maintains a personal grudge against every typosquatted package she has ever had to forensically dissect.