September 16, 2025·6 min read·engineeringpythonsecuritycisupply-chain

PyPI revokes tokens stolen through injected GitHub workflows

GhostAction used repository workflow changes to extract publishing credentials. PyPI revoked affected tokens and found no attacker publications, while recommending short-lived publishing identities.

A repository secret is protected from casual reading, but a workflow authorized to use it must receive its value. An attacker who can add executable workflow code may therefore turn a stored credential into an outbound request. The secret does not have to appear in a committed file or in a public log for the compromise to cross into another service.

PyPI's September 16 response describes this route to publishing-token theft. Attackers injected code into GitHub Actions workflows to send tokens to external servers. Some tokens were exfiltrated, but PyPI found no attacker-published packages and invalidated affected credentials. The registry itself was not compromised.

The significant boundary is between control of a repository's executable automation and authority over its packages. A token connecting those systems can extend a repository compromise beyond the repository. Revocation breaks that credential's continuing authority, even if a copy remains outside the original system.

I dislike the amount of operational machinery around Python packaging. That makes the distinction between necessary and avoidable credentials particularly relevant. A long-lived secret stored in automation creates an obligation that does not disappear when the packaging command finishes.

Workflow code requested the secret

GitGuardian's initial GhostAction research explains the observed mechanism in the FastUUID repository. A malicious commit added a workflow presented as a security operation. Its command referred to the PyPI secret by name and sent its value in an HTTP POST to an attacker-controlled endpoint.

The researchers could identify secret names from legitimate workflow configuration. That configuration already needed to reference the credentials it used for publishing. The attack did not require the value to be visible in source before the malicious workflow ran.

This is different from a compromised reusable action silently changing its implementation. Here the evidence concerns workflow files added to repositories through compromised access. Both mechanisms can expose automation credentials, but preventing one does not establish protection against the other.

The research also describes a campaign spanning multiple credential types and repositories. That wider scope explains why a Python registry participates in an incident originating elsewhere. A workflow may hold authority for more than one package index or deployment destination.

For analysis, a secret's location is less informative than the code allowed to request it. Encryption at rest protects one stage of its lifecycle. Execution authority determines what happens when automation needs the plaintext value.

A successful theft is not a successful publication

PyPI's report distinguishes credential exposure from observed registry abuse. It says the affected tokens did not appear to have been used on PyPI. That is stronger information than simply naming packages at risk, but narrower than proving every affected system was unharmed.

Repository maintainers and a package registry observe different events. The repository can contain evidence that a workflow sent a credential elsewhere. The registry can examine use of that credential within its own operations. A report about one system should not silently claim visibility into the other.

This matters when deciding what the incident means for package consumers. Token theft creates the possibility of unauthorized publication. It does not establish that a downloaded release was malicious. A consumer needs evidence connecting an artifact to the compromise before making that claim.

Credential revocation is still appropriate when no publication is observed. It removes an available route for future use. Waiting for a malicious upload would turn containment into a response that depends on the attacker first exercising the stolen authority.

Log masking is a different protection

GitHub's workflow security guidance distinguishes secret handling from control over executable workflow code. It warns that users with repository write access can obtain configured repository secrets, and recommends minimizing credential privileges.

The guidance also describes log redaction and its limits. Redaction protects output channels where accidental printing might reveal a secret. It cannot prevent a deliberately authorized process from using a credential in a network request. The GhostAction mechanism makes that difference concrete.

A workflow that never prints a publishing token can still misuse it. The absence of plaintext in a log is therefore not evidence that the token stayed within its intended trust boundary. It establishes only something about the recorded output.

GitHub also describes required reviewers for environments as a way to control access to environment secrets. Such a gate can add a decision before a publishing job acquires authority. Its usefulness depends on the configured boundary and what code receives approval, which is a separate question from whether a repository has any review rule at all.

Trusted Publishing changes the credential lifecycle

PyPI's Trusted Publishers announcement describes an alternative to a manually generated, persistent API token. A configured OpenID Connect identity is authorized to request a short-lived, scoped PyPI token. The automation no longer needs a reusable PyPI secret stored in the external service.

The configuration ties that authority to an expected publisher, such as a repository and workflow. Optional environment restrictions can require manual approval for the release job. These controls make the identity exchange a policy decision about an execution context.

This reduces the lifetime and distribution of a credential that an attacker could copy. It does not imply that executable automation becomes harmless. A token issued to a legitimate job remains useful while valid, and control over that job remains consequential.

PyPI's incident response explicitly notes that Trusted Publisher tokens can also be exfiltrated. The reason to prefer them is the reduced risk created by scope and expiry. Calling the arrangement secretless should not obscure the existence of temporary credentials during the exchange and upload.

My preference is to minimize authority that persists between runs. A release workflow needs permission while it publishes. Keeping an equivalent reusable credential available indefinitely expands the time during which a copied value can matter.

Provenance answers another question

PEP 740 supplies a complementary mechanism for distributing digital attestations through a Python package index. Its format associates statements with a distribution filename and SHA-256 digest, together with verification material.

That binding lets a verifier connect evidence to the exact artifact it examines. A matching project name alone is insufficient because a project contains multiple files and releases. The digest identifies the contents to which the attestation applies.

The verifier must also check an appropriate signing subject. A signature that verifies under an unexpected identity does not satisfy a policy that requires a specific publisher. Identity matching is part of verification, not an optional interpretation of a successful cryptographic operation.

The PEP does not mandate attestations on all uploads or their verification by installers. It establishes transport and representation for evidence. Enforcing an expected publisher remains a consumer policy, and authenticated provenance does not establish that the resulting program is benign.

PEP 740 serves provenance separately from distribution files. A client can obtain the associated evidence before it processes an archive, and the format applies to both source distributions and wheels. Those choices help verification fit existing package formats.

This complements short-lived publishing identities without replacing workflow protection. An attacker executing inside the authorized publishing context can create a problem that still has the expected origin. Provenance can identify that origin accurately while the origin's code is compromised.

The distinction is useful in this campaign because the reported outcome stops at token exposure and containment. No malicious PyPI artifact was identified by the registry's response. Attestation mechanisms explain what evidence consumers can use in general, but they should not be presented as forensic evidence about an upload the report says did not occur.

Release authority begins before the upload

GhostAction places workflow changes within the authority model of a package release. A small configuration addition can be executable code with access to credentials for another system. Reviewing that change is therefore a decision about release authority, even when it contains no package source changes.

The response needs to account for both the malicious execution path and the credential it exposed. Removing a workflow prevents that particular code from running again. Revoking a copied token prevents its later use outside the workflow. Each action affects a different part of the attack.

PyPI's response provides the registry-side outcome, token invalidation with no observed attacker publications. The longer-term improvement is to make each publishing run acquire only the authority its configured identity needs for the period in which it publishes. That leaves the repository's executable workflow code as an explicit part of the package's trust model.