July 31, 2025·6 min read·engineeringpythonsecurityauthenticationsupply-chain

PyPI phishing turned account access into malicious releases

July's phishing campaign compromised four PyPI accounts and produced two malicious num2words releases. The incident separates browser authentication, publishing authority, and artifact provenance into distinct security problems.

A package registry can operate correctly while accepting a malicious release through a compromised maintainer account. The attacker does not need to replace the registry's infrastructure if the registry already trusts an identity the attacker can use. July's PyPI phishing incident demonstrates that route from a convincing login page to package distribution.

The July 31 incident report identifies four successfully phished accounts and two attacker-created API tokens. The tokens were revoked, and the affected accounts were disabled or had credentials rotated. Attackers uploaded two malicious releases of num2words, which the owner removed. PyPI says its own infrastructure was not breached.

That distinction determines the scope of the problem. Registry integrity cannot, by itself, establish that every authorized publisher is acting legitimately. An account takeover gives an attacker a path through operations intended for a maintainer.

I already find Python packaging more complicated than it should be. Incidents like this make the complexity harder to dismiss because several different trust relationships need protection. A single assurance that a package came from PyPI says little about how the publisher obtained authority to upload it.

The browser displayed the real site's content

The initial July 28 warning describes messages purporting to request email verification. They used pypj.org, with a lowercase j where the legitimate domain has an i. The linked site presented a login page and passed requests through to PyPI.

Forwarding requests matters because the attacker can present responses produced by the legitimate service. A user may see the expected page and an apparently successful login while submitting credentials through an attacker-controlled origin. Visual resemblance is not the only deception. The interaction can use real service behavior.

The warning directs affected users to change their password and inspect account security history. Those steps address account access. They also establish why a response needs to examine changes already made through the account, rather than stopping after the login secret changes.

The follow-up report explains that a proxy could relay a time-based second-factor code while it remained valid or obtain a session cookie returned during login. It presents those as possible mechanisms. The report does not establish which exact path applied to every compromised account. Treating that uncertainty explicitly avoids turning a plausible explanation into a forensic claim.

Domain binding changes the authentication mechanism

The WebAuthn Level 2 specification defines credentials scoped to a relying party identifier. A credential can authenticate to the entity identified by the RP ID for which it was registered. The browser constrains that identifier according to the caller's origin.

A similar-looking domain does not acquire the legitimate domain's identity by rendering the same page. A PyPI credential scoped to the legitimate service cannot simply be requested by a page on the phishing domain. This provides a protocol-level distinction that a manually entered code does not carry.

The specification also describes random server challenges and verification of the returned challenge. Those rules prevent a captured assertion from becoming a generic reusable proof. Authentication depends on the expected ceremony and service identity.

This does not establish immunity to every account compromise. Device compromise or malicious code running within a legitimate origin presents a different problem. The relevant property here is narrower and useful, preventing an unrelated lookalike origin from obtaining an assertion for the legitimate relying party.

For maintainers, the distinction is between mechanisms with different security properties, even when both appear under a two-factor setting. Counting factors is insufficient if the attacker can relay the entire interaction. A browser-enforced service binding addresses a different failure than a second secret that a user can type into any form.

Removing a release does not inspect installations

The PYSEC-2025-72 advisory identifies num2words versions 0.5.15 and 0.5.16 as the malicious releases. It advises users to remove affected versions from their environments. Naming the exact versions gives consumers and tools a concrete condition to examine.

Registry removal prevents those artifacts from remaining available through their original distribution path. It does not inspect an environment that already received them. That follows from the difference between controlling a registry and controlling every downstream installation.

The advisory is therefore more actionable for consumers than a broad warning about the package name. The question is whether a particular environment contains an affected artifact. A dependency that happens to share the project's name is not sufficient evidence that the malicious release was installed.

The public record supports that package-specific investigation. It does not establish the number of affected downstream systems or prove what any particular installation executed. Those facts require evidence from the environment in question.

Publishing credentials are a separate concern

PyPI's 2023 introduction of Trusted Publishers describes a different approach to automated publishing. A configured identity provider supplies a short-lived OpenID Connect identity token, which PyPI uses to authorize short-lived, scoped publishing credentials.

This removes the need to store a long-lived PyPI API token in the external system. Trust attaches to a configured publishing identity, such as a repository and workflow. The announcement also describes optional environment restrictions that can require approval before a workflow publishes.

That changes the handling of automated credentials, but it does not make an account takeover irrelevant. Administrative authority over a project is a separate capability from one workflow's authority to upload. The incident's attacker-created tokens show why examining account changes belongs in the response.

My preference is to evaluate those capabilities separately. Human login, project administration, and release automation each deserve a defined authority model. Combining them under a single notion of package trust makes it harder to explain which control prevented, or failed to prevent, a specific action.

Attestations provide evidence about an artifact

PEP 740 defines how a Python package index accepts and distributes digital attestations. This is relevant context for the incident because it provides a way to examine an artifact's claimed origin beyond unauthenticated project metadata. It is not evidence that the affected releases carried attestations.

The format binds a statement to one distribution filename and its SHA-256 digest. Verification requires the digest to match the distribution and the signature to match the certificate. It also requires the certificate to identify an appropriate signing subject, such as the expected publishing identity.

That last requirement is substantive. A mathematically valid signature only establishes that a particular key signed a statement. A consumer needs policy connecting that identity to the package it intended to obtain. Otherwise, verifying a signature can confirm the wrong publisher just as successfully as the right one.

The PEP provides provenance objects that associate attestation bundles with publisher information. Repository or workflow context can then inform a consumer's decision. The index exposes provenance locations through its repository APIs, allowing clients to retrieve the evidence for individual release files.

The specification does not mandate attestations for every upload or require installers to verify them. Availability of a protocol is therefore different from deployment of an enforcing policy. A consumer cannot assume that installing a package implies an attestation was present and checked.

PEP 740 also allows provenance objects to change over time, for example when another signing identity adds evidence about an existing artifact. The distribution digest remains necessary to associate that evidence with the intended bytes. A consumer retaining provenance needs to distinguish the artifact identity from the set of statements currently available about it.

Attestations also do not prove that source code is benign. A release produced by an authorized workflow can still contain a malicious source change or compromised build input. Provenance helps answer where an artifact came from under a particular identity and process. Code review and protection of that process remain separate concerns.

The incident crosses distinct authorities

The connection among these mechanisms is useful because each addresses a different question. WebAuthn binds a login credential to a service identity. Trusted Publishing reduces persistent secrets in automation. Attestations bind artifact evidence to a signing identity and digest. None should silently stand in for all the others.

For this incident, the confirmed outcome is malicious publication through compromised maintainer access, followed by account action, token revocation, and release removal. The existing security mechanisms explain how to strengthen different stages without claiming they were all present in the affected project. Consumers still need the exact affected versions when determining whether an environment received the malicious artifacts.