Safeguard
Supply Chain Attacks

Anatomy of a PyPI Compromise: How durabletask Got Hijacked in 35 Minutes

Three malicious durabletask releases hit PyPI in a 35-minute window in May 2026 — a maintainer-token theft, not a code review failure.

Safeguard Research Team
Research
6 min read

On 19 May 2026, three versions of Microsoft's durabletask SDK — 1.4.1, 1.4.2, and 1.4.3 — landed on PyPI within a roughly 35-minute window. None of them came from Microsoft's build pipeline. An attacker had compromised a GitHub account belonging to a maintainer with PyPI publishing rights, pulled a long-lived PyPI API token out of that account's accessible repository secrets, and used it to push straight to the registry — skipping CI, skipping code review, skipping every gate Microsoft's engineering process normally enforces. On import, the trojanized package fetched a second-stage payload named rope.pyz that harvested AWS, Azure, and GCP instance-metadata credentials, Kubernetes service-account tokens, and HashiCorp Vault tokens, attempted to brute-force local password-manager vaults, installed a persistent systemd unit (pgsql-monitor.service), and shipped a geo-targeted destructive wiper. Independent researchers at StepSecurity and Wiz tied the campaign to the threat actor tracked as TeamPCP, based on infrastructure and payload overlap with earlier compromises of packages including @antv and guardrails-ai. PyPI pulled the malicious releases and Microsoft shipped a clean 1.5.0. This post walks through exactly where the chain broke, and which specific controls would have interrupted it at each link.

What actually failed in the durabletask compromise?

The failure wasn't in durabletask's source code — it was in publishing infrastructure. The attacker never touched Microsoft's GitHub Actions pipeline or opened a pull request; they compromised a maintainer's GitHub account, found a PyPI publishing token sitting in that account's reachable repository secrets, and used it directly against PyPI's upload API. That single static credential was sufficient to publish three releases with full legitimacy in the eyes of pip install, because PyPI has no way to distinguish "a token used by CI" from "the same token used by whoever is holding it." Wiz's writeup on the incident notes the attacker moved from account access to three published releases in well under an hour — fast enough that Microsoft's own security team learned about the compromise from public reports before internal telemetry caught it.

Would mandatory 2FA have stopped this?

Partially, and only if the attacker's path had gone through the PyPI login UI rather than around it. PyPI began requiring two-factor authentication for all maintainers in 2023, specifically as a response to a wave of account-takeover incidents that trojanized popular packages via stolen passwords. 2FA is genuinely effective against credential-stuffing and phishing attacks that try to authenticate as the maintainer on pypi.org — it would have blocked a login attempt cold. But the durabletask attacker didn't need to log in to PyPI at all. They exfiltrated a standing API token from GitHub repository secrets and called PyPI's upload endpoint with it directly. PyPI tokens, once issued, don't re-check the second factor on every use — 2FA protects the login, not the token's entire lifetime. That's the exact gap the next control closes.

Why does Trusted Publishing eliminate this class of attack outright?

PyPI's Trusted Publishing feature removes the static, long-lived token from the equation entirely. Instead of a maintainer generating an API token and storing it as a CI secret indefinitely, Trusted Publishing lets a specific GitHub Actions workflow (identified by repo, workflow file, and environment) request a short-lived, single-use publishing credential from PyPI via OpenID Connect at the moment of a real release run. There is no persistent secret sitting in repository settings for an attacker to steal, because none exists — the credential is minted per-run and expires within minutes. Had durabletask been configured for Trusted Publishing instead of a classic API token, the compromised GitHub account would have given the attacker no path to PyPI at all: OIDC-issued credentials are scoped to the exact workflow file and can't be replayed by a human holding stolen account access. This is the single control in the chain that converts "account takeover" from a publishing risk into a non-event for the registry.

How would Sigstore signing and PEP 740 attestations have exposed the malicious releases?

Even without preventing the upload, cryptographic provenance would have made the malicious releases visibly anomalous within minutes. PEP 740, which reached general availability on PyPI in November 2024, lets projects publish digital attestations for each release, signed via Sigstore's Fulcio certificate authority using short-lived certificates bound to the actual CI workload's OIDC identity, with every signing event recorded in the public Rekor transparency log. A release built by Microsoft's legitimate GitHub Actions pipeline would carry an attestation naming that workflow as the signer. A release pushed by an attacker with a stolen token — bypassing CI entirely — would either carry no attestation at all or one signed by a completely different, unrecognized identity. Either state is a loud, publicly queryable anomaly: any consumer or scanner checking Rekor against the expected signer for durabletask would see the mismatch immediately, well before a human noticed the version bump.

Could package-integrity tooling have caught the payload itself, separate from the identity problem?

Yes — behavioral classification of the release contents is a complementary layer that doesn't depend on the publishing path being compromised at all. A package that fetches a remote binary payload on import, reaches out to attacker infrastructure, reads cloud instance-metadata endpoints, and installs a persistent systemd service exhibits a cluster of behaviors that legitimate SDK releases essentially never do. Safeguard's Eagle model, documented in Safeguard's malware-detection capabilities, classifies every PyPI publish — including retroactive scans of existing packages — against indicator classes that cover exactly this pattern: install-script behavior that contacts unusual hosts, credential-harvesting reads, and behavior divergence from a package's own prior versions. A durabletask 1.4.1 that behaved nothing like 1.4.0 on import is precisely the divergence signal that class of detection is built to surface, independent of whether the upload itself looked "authorized."

What should teams actually change after an incident like this?

Three concrete changes close this gap, and none of them require waiting on the registry. First, migrate every package you publish from static PyPI API tokens to Trusted Publishing — it is a one-time GitHub Actions configuration change and it removes the stealable credential permanently. Second, require and verify Sigstore attestations (PEP 740) on packages you consume, not just ones you publish, so an out-of-band release shows up as a signer mismatch instead of a silent trust decision. Safeguard's attestation and signing pipeline verifies signer identity and SLSA provenance level against policy at both CI time and admission time, so a mismatch blocks automatically rather than depending on someone noticing a GitHub advisory. Third, treat every maintainer's personal account — not just organizational CI — as in-scope for the same 2FA and least-privilege review you'd apply to a production credential, because durabletask proves the weakest link is often a human account sitting several steps upstream of the registry itself.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.