PyPI and GitHub Are Racing Against Time for Package Poisoning
Published on gastropod.io on 07-28-2026
PyPI and GitHub Are Racing Against Time for Package Poisoning
Tags: software supply chain security, vulnerability intelligence, pypi, python, package poisoning, provenance, github, ci-cd security
I was considering writing about two separate supply chain changes tonight & relized I realized they are essentially the same thing. PyPI and GitHub both took similar actinos this week, from different ends, and neither one touched the root cause.
PyPI now rejects new file uploads to any release older than 14 days. This is in response to recent package poisoning attacks where an attacker who steals a project's publishing token or hijacks its release workflow could insert and upload malicious code to an old, widely trusted version without changing the version number. Because the version stayed the same, routine dependency updates would pull the poisoned file and nobody would see a new release to inspect. PyPI says it has no evidence this was exploited, and that the only thing stopping it was attackers not realizing it was possible. The changes were merged on July 8, 2026.
This likely happened due to real world incidents such as what happened in March 2026 after the LiteLLM and Telnyx packages were compromised through a mutable reference in their use of the Trivy GitHub Action. PyPI measured the blast radius across the top 15,000 projects and concluded 56 had added a Python 3.14 wheel to a release more than 14 days after it first went out. Maintainers who need to support a new Python version now publish a new version instead of editing an old one.
GitHub moved in a similar direction with Dependabot which they set to now wait at least three days after a release is published before it opens a pull request for a non-security version bump. The reasoning is that most malicious versions get caught and pulled within a few days, so a short delay keeps the poison out of your repository in the first place. The cooldown is configurable in dependabot.yml and does not apply to security updates.
Both approaches are sensible & incorporate time windows as part of their trust model.
Here is what actually made me cringe a bit... the version number is not completely fixed. "requests 2.31.0" was, until this month, a label that could have updates added to it later. A Git tag can be force-pushed. "latest" resolves to whatever was published most recently. Attackers can still exploit the gap between the name a team trusts and the actual code that name delivers. PyPI's 14-day lock and GitHub's 3-day cooldown both narrow that gap a lot but it is still there. Fourteen days is an estimate. Three days is an estimate. PyPI even notes you cannot rely on the lock programmatically yet, because there is no API to tell you a release is closed.
Where identity replaces the timer
The real/durable fix is to identify software by its content, not by its name and version. That is how gastropod.io works. Every artifact is content-addressed and identified by its sha256 digest, and the same digest is the same artifact everywhere it appears across your organization. A malicious bit of code added to an old release is hashed mathematically as a different digest, so it arrives as a new, unverified artifact, not as an invisible edit to the version you already cleared.
Gastropod verifies provenance at intake, not on a delay: Go's checksum database, npm SRI, and Sigstore attestations where they are present. For a project that publishes with attestations or Trusted Publishing, a file pushed outside that flow does not carry valid provenance, and Gastropod flags the mismatch when the file enters, not three days later. This is something you can apply policy to as a control.
When a bad artifact does turn up, the question PyPI is trying to make answerable, which files are compromised and who pulled them, is a single query. Gastropod records where every digest has been seen, maps its direct and transitive dependents, and attributes every pull to a principal.
Time-based defenses lower the odds. Identifying artifacts by content removes the ambiguity the timers are working around. You can see how Gastropod handles artifact identity and provenance, and run it against your own PyPI and npm traffic when you get started.