DEV Community

Cover image for Miasma worm, Part 2: how a GitHub token survived a full machine rebuild and hijacked my repos from an Azure IP
Ionut-Cristian Florescu
Ionut-Cristian Florescu

Posted on

Miasma worm, Part 2: how a GitHub token survived a full machine rebuild and hijacked my repos from an Azure IP

How the lockout ended, the entry vector GitHub confirmed, the stealth commits I missed at first, and the cleanup playbook for anyone hit by the same worm.

By Ionut-Cristian Florescu (@icflorescu), written June 8, 2026.

This is the follow-up to Part 1, which I wrote on June 6 while I was still locked out of my account with the Miasma worm's payload live in my repositories. The short version of what changed: it is over, the repositories are clean, and the forensics turned a careful "I can't rule this out" into a confirmed chain of events. Here is the rest of the story.

1. How it ended

I was suspended on the morning of June 4. On June 8 at 07:50 UTC, a GitHub support agent restored the account, forced a password reset, and handed the ticket to a second agent for the technical questions. That was, in total, roughly four days of being locked out of a sixteen-year-old account during an active incident I had reported myself.

I won't pretend I know exactly what moved it. What I can say is that the resolution came after the story went public, after the write-up, the Hacker News thread, and security researchers like Adnan Khan calling it out. Whether that mattered or it was simply my place in the queue, I can't prove. But the lesson I draw is the same one Part 1 ended on: a verified, evidence-complete, actively-dangerous case should not need to go viral to get worked.

Once I was back in, removing the payload took an afternoon. The how is in section 5.

2. The entry vector, now confirmed

This is the part I could only theorize about in Part 1, and the part GitHub's logs settled.

The commits were made with my GitHub CLI (gh) OAuth token, created on 2026-01-17, used from IP 52.240.186.x, a Microsoft Azure range, via the GitHub API (the support team found API events only, no Git operations). Three things fall out of that:

  • It was a stolen credential of mine, not a breach of my password or 2FA. Exactly as Part 1 argued: 2FA never gated this, because token and API writes don't go through interactive login.
  • It explains the forged, unsigned commits. The commits were created through the API, where the caller sets the author field freely and nothing is server-signed. That is why they show up as github-actions (or, on side branches, as me) and as verified: false. My Part 1 phrasing of "a plain git push" was the one technical detail I got slightly wrong: it was the API, not the git protocol. The conclusion, a forged author on an unsigned commit, was right.
  • It confirms the disclosure I was most worried about being wrong. That gh token was created in January, predating the from-scratch rebuild of both my machines in mid-May. A reinstall gives you new local credentials but does not revoke the old ones on GitHub's side, so the January token stayed valid and was reused on June 3. In Part 1 (section 6.4) I wrote that "the wipe did not close this door" and that I couldn't rule out a pre-rebuild credential theft. It turns out that is precisely what happened.

Where the token was harvested in the first place is the one link still inferred rather than proven. The strongest candidate, raised by security researcher Daniel Ruf and consistent with the campaign's documented method, is a compromised dependency in the TanStack supply-chain wave. I run a project on TanStack Start, the timing fits, and the same worm reused a Microsoft contributor's credential that traced back to a compromise a month earlier. I can't put a specific npm install to it, and I won't pretend to.

3. The stealth I missed at first

In Part 1 I described five repositories, each hit once on main with a commit forged to look like the github-actions bot. That was only half of it.

When I came back and actually scanned, I found that every repository was hit on a second branch too, with a far sneakier disguise. The side-branch commits were forged under my own name, given plausible messages ("Update deps", "Bump version", "Update V6 contributors"), and backdated so they read as old, routine work on a quiet branch:

Repo (side branch) Forged author Backdated to
mantine-datatable / next me 2026-05-20
mantine-contextmenu / next me 2026-05-14
next-server-actions-parallel / next me 2025-01-09
mantine-datatable-v6 / next me 2024-01-17
mantine-contextmenu-v6 / next me 2023-11-10

Look at the dates. They get progressively older, and the messages get tailored per repo. Whoever ran this took the trouble to make each stealth commit blend into that specific project's history. If I had trusted the obvious github-actions wave and stopped there, these would still be sitting in my repos, dated like commits I made years ago.

The practical lesson is blunt: do not detect these by author. A backdated commit forged under the maintainer's own name passes every "is this the bot?" check. Detect by the payload file instead. The reliable test across all branches is whether .github/setup.js exists:

git fetch origin
git for-each-ref --format='%(refname:short)' refs/remotes/origin | while read b; do
  git cat-file -e "$b:.github/setup.js" 2>/dev/null && echo "INFECTED: $b"
done
Enter fullscreen mode Exit fullscreen mode

4. The blast radius was wider than five repos

Part 1 was about icflorescu. The same payload, dated inside the same 49-second June 3 window, also landed in a repository under a separate organization account I control, again on both branches, again with the two-disguise pattern. I am still sweeping the rest of my accounts and will update if more turns up. The takeaway for anyone reading: if one of your accounts was hit, assume the worm walked every repository every harvested token could reach, and scan all of them.

5. The cleanup playbook

If you are cleaning up the same thing, here is what worked, and the one rule that matters: never let an affected repo execute. No opening it in an AI editor, no npm install, no npm test. A no-checkout clone keeps the payload off your disk entirely.

# Scan every branch for the payload (see section 3).

# Evidence backup once, as a bare clone (no working tree, nothing runs):
git clone --mirror https://github.com/<you>/<repo>.git evidence.git
tar czf evidence.tar.gz evidence.git

# Work clone with no checkout:
git clone --no-checkout https://github.com/<you>/<repo>.git fix
cd fix

# For each infected branch, confirm the malicious commit is the tip, then
# reset the branch to its parent (drops the commit out of history):
git push --force-with-lease origin <MALICIOUS_SHA>^:refs/heads/<branch>
Enter fullscreen mode Exit fullscreen mode

Two deliberate choices. First, I reset rather than git revert: a revert leaves the 4.3 MB dropper retrievable at the old commit SHA, and for live malware you want it gone, not archived. The attack is preserved where it belongs, in this write-up and in GitHub's logs, not as a live blob in my history. Second, the reset alone is not the end: because of the fork network a commit can stay reachable by SHA after it is off every branch, so I gave GitHub Support the full list of malicious SHAs and asked them to garbage-collect and purge them via the sensitive-data removal process. That is the definitive kill.

And the reassurance that has not changed since Part 1: the npm packages were never touched. No malicious version was ever published. Everything above is about the GitHub source repositories. If you install my packages from npm, none of this ever reached you.

6. Credit where it's due, and the gap that remains

I was hard on GitHub in Part 1, and I stand by every factual word of it. But fairness cuts both ways, so: the support engineers who eventually worked this, especially the one who pulled the API logs, were excellent. The forensics they provided, the token identity, the source IP, the API-versus-git distinction, are exactly what let me write this with confidence instead of speculation. When a human finally engaged, the engagement was good.

The gap was never the people. It was the days of silence before one of them got to it, and the automated suspension that locked the victim out of his own incident while leaving the weapon live. Both of those are process, not personnel, and process is what I hope GitHub looks at. A verified owner reporting an active payload in his own public repos should be a fast lane, not a four-day queue.

7. What I'm changing

  • Long-lived classic tokens are gone. Fine-grained, least-scope, short-expiry tokens only, and I revoke anything I am not actively using.
  • Required signed commits and branch protection on the repos, so an unsigned forged push like these cannot land on a protected branch in the first place.
  • GitHub Actions pinned to full commit SHAs, GITHUB_TOKEN scoped to read-only by default.
  • A standing habit of scanning my own repos by payload file, not by author, after any ecosystem-wide supply-chain wave.

8. A warning to other maintainers, and the questions I'm left with

If you take one operational lesson from my four days, take this: your account can be taken from you by an automated system, at the worst possible moment, with no human in the loop and no fast way back. I was not suspended for anything I did. An automated process saw anomalous activity on my repositories and locked the owner out, which meant the one person most motivated to pull the payload down, me, was the one person who could not. If your livelihood runs through a single platform account, that is a single point of failure you do not control. Mirror your repositories somewhere independent (I now push to Codeberg and GitLab alongside GitHub), keep local clones current, and do not assume that having done nothing wrong will keep you logged in.

That leads to questions I don't think are mine alone to answer, so I will pose them rather than pretend to settle them.

At GitHub's scale, has automation been forced to act faster than humans can judge, with the cost being a system that sometimes punishes the victim in order to contain the threat? When an automated mitigation locks a maintainer out of his own live incident, is it containing the spread, or quietly adding to it, by removing the one person who could revert the payload while leaving the payload up? And the newer question: this attack weaponized AI coding agents as its execution vector, and it unfolded during an industry-wide rush to put AI into every developer tool, faster than any of it is being secured. Are we shipping attack surface faster than we can defend it?

I don't have clean answers. I have one data point, my own, and a strong suspicion that I am not the last person this will happen to. These deserve more than a closing paragraph, so I will come back to them properly in a separate piece. For now I will only say that an ecosystem this important should be able to tell a victim from an attacker in less than four days.

9. Closing

Part 1 was written by someone in the middle of it, locked out and angry, and I left it unedited on purpose. Part 2 is written by someone on the other side, with the logs in hand. The arc I take from the whole thing is simple: I did the careful things and still got hit, because a token I created in January outlived the machine I created it on. The attacker was patient and meticulous, down to backdating commits by years. GitHub's automation made the victim's bad day much worse, and its people, once reached, made it right.

If you maintain open source, the one habit worth taking from this is the smallest one: treat the config files of your AI editor as executable code, and check your branches by what they contain, not by whose name is on the commit.

Ionut-Cristian Florescu, June 8, 2026.


Part 1, the original in-the-moment account, is at https://dev.to/icflorescu/the-bot-that-never-was-2mfp (source: https://codeberg.org/icflorescu/miasma-github-incident). The forensics in section 2 are from GitHub Support's response on ticket #4448974; everything about my own repositories is reproducible from the public commit metadata and the decrypted payload.

Top comments (0)