Shai-Hulud exposed Python publishing credentials across ecosystem boundaries
PyPI revoked valid credentials exposed during an npm-focused campaign and found no evidence of malicious use. Shared release infrastructure explains why a JavaScript compromise can create Python publishing risk.
Package ecosystems are separate distribution systems, but a release machine can hold credentials for several of them. An attacker executing in that environment does not have to respect the language boundary that organized the code. A campaign entering through JavaScript can discover authority to publish Python software.
PyPI's November 26 account of Shai-Hulud describes that exposure. Researchers found valid, long-lived PyPI credentials in compromised repositories associated with the npm-focused campaign. PyPI revoked them as a precaution and contacted affected users. It found no evidence that the credentials had been used maliciously.
The report notes that repositories may publish packages to both npm and PyPI. The affected PyPI accounts had not published recently, but their credentials remained valid. Inactivity therefore did not remove the publishing authority available to whoever obtained those values.
I find Python's packaging systems frustrating, especially the operational work surrounding an otherwise small program. This incident is a reason to remove obsolete release credentials as part of simplifying that work. An unused secret remains an administrative responsibility while the registry continues to accept it.
Shared infrastructure joins separate registries
The useful scope for understanding this exposure is the environment that runs release code. A repository may group multiple languages for sensible reasons, including a shared product or coordinated release. Its automation can then hold permissions for destinations that are otherwise independent.
That creates a relationship between the systems even if neither registry depends on the other. The relationship is the credential stored where compromised code can reach it. An attacker does not need to exploit the Python registry to gain a valid route into its upload operations.
The absence of observed malicious use is also a meaningful boundary in the report. Exposure creates risk, while registry activity determines whether a credential was exercised there. The published evidence supports precautionary revocation. It does not support calling every associated Python package malicious.
A response limited to the ecosystem named in the campaign would miss this relationship. The appropriate inventory follows the authority available in the compromised context. That is an architectural consequence of placing multiple release capabilities in one environment.
Short-lived identities reduce persistent authority
PyPI's Trusted Publishers design replaces an externally stored, long-lived publishing secret with an identity exchange. A configured OpenID Connect identity can request a short-lived, scoped token for an upload.
The persistent configuration describes which publisher PyPI trusts. The credential used during execution expires, so the system does not need to retain an equivalent reusable secret between release runs. That changes what can be stolen from storage and how long a copied token remains useful.
The design also allows a configured GitHub environment to restrict publication. Required approval can put a decision between a workflow reaching its publishing job and receiving access to its release authority. The environment must participate in the trust configuration for that restriction to describe the intended publisher.
Short lifetime does not remove the need to protect a running job. Code executing in an authorized publishing context may still misuse authority while that context is active. The benefit is the reduction of persistent credentials and their continuing value after exfiltration.
An earlier Python incident separates two paths
PyPI's December 2024 Ultralytics analysis provides a concrete precedent. Its first malicious releases came through the project's existing Trusted Publishing workflow after compromise of the build process. Provenance evidence helped investigators narrow the possible source of the injected code.
A later set of malicious releases used an unrevoked API token still available to the workflow. The absence of corresponding repository activity and publish attestations distinguished that second path. Migrating the normal release route had not removed the older credential.
This history explains why adding a safer publishing mechanism is only part of a migration. A superseded credential continues to grant authority until it is revoked. The new route can operate correctly while an old route remains available to an attacker.
The November exposure involves a different campaign and does not establish the same sequence. The earlier analysis is relevant because it demonstrates the architectural failure of leaving persistent authority behind after the routine workflow changes.
Approval needs a defined object
The March pyOpenSci publishing guide describes using a dedicated GitHub environment with required reviewers and connecting that environment to the PyPI Trusted Publisher. It also recommends workflow analysis with zizmor and removal of unused tokens.
Those controls apply to different objects. Environment approval gates a job. A publishing identity constrains who can request credentials. Static analysis examines workflow configuration for known hazardous patterns. Removing an old token eliminates authority that none of those mechanisms necessarily revokes.
The distinction helps avoid a false conclusion that one configured protection covers the complete release process. A human can approve a job whose inputs were already compromised. A valid identity can publish output produced by malicious build code. Each decision needs an explicit scope.
My preference is for a release process whose persistent permissions are easy to enumerate. That makes an incident's cross-system implications easier to determine. It also makes an idle credential visible as something to remove, without waiting for its project to publish again.
Artifact identity remains separate from account activity
PEP 740 defines attestations that associate publishing evidence with a distribution filename and digest. That lets consumers and investigators ask about the origin of exact artifact bytes, in addition to examining registry-account events.
The specification does not mandate attestations for every upload, and a valid attestation does not prove benign code. Its role is to make a claim about an artifact verifiable under an expected identity. This adds evidence to a release process without replacing protection of that process.
For the November report, the immediate result is credential revocation with no evidence of malicious PyPI use. The broader implication is a concrete inventory requirement. A shared publishing environment has authority over every destination whose valid credentials it contains, including destinations outside the ecosystem through which the attacker entered.