Skip to content

PyPI Moved 1.92 Exabytes Last Year. Its Safety Team Is One Person.

May 20, 2026·Tim Hopper

On July 26, 2025, PyPI maintainers received phishing emails pointing at pypj.org. The domain proxied the real PyPI login page in real time. The address bar showed hxxps://pypj.org/, the lowercase j close enough to an i that four maintainers signed in anyway. The proxy captured each TOTP code in the fraction of a second PyPI accepted it, logged in, generated API tokens, and uploaded two malicious releases of num2words (0.5.15 and 0.5.16) before anyone noticed. PyPI shipped a phishing-domain detector on July 28 at 11:37 EDT and revoked the tokens; the registrar parked the domain shortly after.

That incident is what PyPI looks like in 2026: cloud-scale infrastructure under attack from people who understand the scale, defended by a team smaller than most companies’ on-call rotation.

The numbers are exabyte-class

PyPI’s own 2025 year-in-review puts the index at:

  • 130,000 new projects created in 2025
  • 3.9 million new files published
  • 2.56 trillion requests served, averaging 81,000 per second
  • 1.92 exabytes transferred

At the Packaging Summit at PyCon US 2026, Mike Fiedler (the PSF’s full-time PyPI Safety & Security Engineer) reported that new-project creation runs between 8,000 and 24,000 a month. The index holds roughly 36 TB of live package files; another 36 TB sits in cold storage as the residue of deleted releases and abandoned uploads.

Fiedler is the only full-time safety hire. Triage work runs on him plus a handful of volunteer PyPI admins and outside reporters, and that arrangement fielded more than 2,000 malware reports in 2025, closing 66% within four hours and 92% within 24. Trusted Publishing has crossed 50,000 projects and now accounts for more than 20% of all file uploads.

A four-hour median is not the same as a closed window

Response time only matters after detection. The num2words releases sat on PyPI for hours before anyone reported them. Every pip install num2words in that window pulled the malicious build.

A compromised maintainer account exploits the same window. PyPI blocks same-name overwrites but allows publishers to delete a release and re-upload under the same version. PEP 592 yanking handles the legitimate “this release is bad” case without removing the file, so the delete-then-reupload path is mostly an attacker tool: phish a maintainer, delete 0.5.16, push a malicious 0.5.16 in its place, and every fresh pip install num2words==0.5.16 in the window fetches the attacker’s wheel. Yanking handles cleanup but not the hours between upload and discovery.

Fiedler’s summit proposal closes the window structurally: lock a release seven days after publication, and route replacement work through a staging mechanism instead of overwriting the public artifact. The grace period covers the legitimate “I uploaded the wrong wheel” case. The lock removes “compromise a maintainer and rewrite an old release” from the attacker playbook.

PEP 694 turns uploads into sessions

PyPI cannot ship lock-after-delay on its own. Every twine upload is final the moment the file lands; there is no notion of a draft release. PEP 694, in its second round of feedback since 2022, replaces the single-step upload with a session: open it, push wheels into a staged release, preview the result through a unique URL, then publish atomically.

  • Test publishes stop needing test.pypi.org. A maintainer points pip or uv at the staging URL via --extra-index-url, installs the candidate, runs the test suite, and only then publishes. The throwaway test index becomes legacy infrastructure.
  • Replacement happens inside the staging session, not on the published version. The maintainer opens a new session to fix a wheel; the public release stays unchanged until they publish. Same-version replacement now requires a session the lock-out can refuse to open, so the “overwrite an old release” path is no longer a single API call.

PyPI has used project quarantine since August 2024 as a shorter-horizon defense: administrators flag a reported project so it disappears from the Simple Index and stops installing while a human investigates. PyPI has quarantined around 140 projects this way. The work in flight automates the trigger (report-volume thresholds instead of human review) and adds a researcher-facing submission API.

Two changes you can make this week

The structural fixes are a year out at minimum. The defenses that stopped the pypj.org campaign are available today.

  • Move from TOTP to a hardware security key. The phishing proxy captured TOTP codes because TOTP is a relayable secret. WebAuthn binds the second factor to the origin in the browser, so a relay from pypj.org cannot present the correct credential to pypi.org. Fiedler’s incident report names WebAuthn as the authenticator type that stopped the attack cold.
  • Adopt Trusted Publishing if you publish from CI. A long-lived API token in GitHub Secrets is the credential the num2words attacker generated and used. Trusted Publishing replaces stored tokens with short-lived OIDC exchanges scoped to a specific workflow in a specific repository. The rationale is one of the few cases where the cheap fix is also the right one.

The structural fixes land in 2027

The summit’s four-part agenda (PEP 694, lock-after-delay, quarantine automation, staged previews) moves PyPI from “respond fast” to “remove the window altogether.” None of it lands in 2026. Until then the publish workflow is the only piece of the supply chain inside your control. The next num2words campaign will reward maintainers already on WebAuthn and Trusted Publishing.

Last updated on