Cloud Security Office Hours Banner

Megalodon / GitHub Actions 2026

Step-by-step kill chain mapped to MITRE ATT&CK Cloud, sourced from official post-mortems and primary technical analyses.

May 2026 Critical GitHub Actions

Megalodon - Infostealer-Harvested GitHub Credentials → 5,718 Automated Commits in Six Hours → Injected Actions Workflows → CI Secret and OIDC Theft Across 5,561 Repos

On May 18, 2026, between 11:36 and 17:48 UTC, an actor pushed 5,718 commits into 5,561 distinct GitHub repositories. Every commit added or replaced a GitHub Actions workflow carrying a base64-encoded bash payload that swept the runner for secrets: AWS credentials and IMDSv2 instance role tokens, Google Cloud access tokens, Azure instance metadata credentials, SSH private keys, Docker and Kubernetes configuration, Vault tokens, GitHub Actions OIDC tokens, GitLab and Bitbucket tokens, and more than thirty regex-matched secret patterns. SafeDep identified the campaign, which researchers named Megalodon. The commits were authored under forged identities like build-bot and auto-ci with commit messages mimicking routine CI maintenance. Hudson Rock's follow-up supplied the origin story: 331 of 978 unique usernames tied to affected repositories, over a third, matched machines already infected by infostealers. There was no clever exploit anywhere in this chain. There was a market for stolen developer credentials, and a script that used them at scale.

5,718 commits into 5,561 repositories in one six-hour window
331 of 978 repo owners matched known infostealer-infected machines
Two payload variants: one loud on every push, one dormant until triggered
Downstream: poisoned Tiledesk npm releases published May 19 to 21
📄 The Hacker News - Megalodon GitHub attack targets 5,561 repos ↗ 📄 SecurityWeek - Over 5,500 GitHub repositories infected ↗
🧰 Resource Development - Buying the Way In
01
GitHub credentials came from infostealer logs harvested off developer machines, not from any weakness in GitHub
T1555.003 - Credentials from Web Browsers T1589.001 - Gather Victim Identity Information: Credentials

Hudson Rock cross-referenced the usernames behind the affected repositories against its infostealer database and found that 331 of 978, more than a third, were direct matches to computers known to be infected. That is the whole initial access story. Commodity stealers scrape saved credentials, session cookies and tokens from developer laptops, the logs are sold in bulk on criminal markets, and somebody bought enough of them to run a campaign. The same economy sits under UNC5537 / Snowflake and under the vendor endpoint that started the Vercel / Context.ai chain. Developer machines are unusually valuable in it, because a developer's browser profile and credential helper hold keys to repositories, registries and cloud accounts all at once.

Source: Commodity infostealer logs from compromised developer machines
Correlation: 331 of 978 unique usernames (over 33%) matched known infected computers
Analysis by: Hudson Rock, published May 23, 2026
Not involved: Any GitHub platform vulnerability
Why developers: One profile holds repo, registry and cloud credentials together
Infostealer LogsCredential MarketDeveloper EndpointT1555.003
Initial Access - Valid Accounts, Disposable Identities
02
The actor pushed through throwaway GitHub accounts with random eight-character usernames, using the stolen credentials as write access
T1078 - Valid Accounts

Pushes came from disposable accounts named with random eight-character strings: rkb8el9r, bhlru9nr, lo6wt4t6 and others in the same shape. The pattern is deliberately cheap, and it is also the campaign's most machine-detectable artifact: a first-time contributor with a high-entropy username pushing directly to a default branch is a two-line rule in any repository policy engine. Nothing was exploited to get here. The credentials were valid, the write permission was real, and GitHub had no reason to refuse the push.

Account pattern: Random eight-character usernames, e.g. rkb8el9r, bhlru9nr, lo6wt4t6
Authorization: Legitimate write access via stolen credentials
Detection opportunity: First-time contributor, high-entropy username, direct push to default branch
Platform exploit: None
Throwaway AccountsValid AccountsDirect PushT1078
🎭 Defense Evasion - Commits That Read Like Housekeeping
03
Commit author identities were forged as build-bot, auto-ci, ci-bot and pipeline-bot, rotating through seven commit messages that imitated routine CI maintenance
T1036 - Masquerading

Git lets the author field be set to anything, so the commits arrived looking like the automation that every repository already ignores. Four bot names and seven maintenance-flavored messages were rotated across 5,718 commits, and the volume distribution shows the intent clearly: two of the forged author addresses account for 2,878 and 2,841 commits respectively. Anyone skimming a repository's activity feed sees CI bots doing CI things. This is the point in the chain where signed commits stop being a nice-to-have. A required signature check does not care what the author field claims, and it would have rejected every one of these pushes.

Forged authors: build-bot, auto-ci, ci-bot, pipeline-bot
Messages: Seven variants, all imitating routine CI maintenance
Distribution: Two forged author addresses carried 2,878 and 2,841 commits
Why it works: The git author field is attacker-controlled free text
Control that ignores it: Required commit signature verification
Forged AuthorsMasqueradingUnsigned CommitsT1036
🔐 Persistence - One Loud Workflow, One Sleeping One
04
A "SysDiag" workflow fired on every push and pull request, while an "Optimize-Build" variant replaced existing workflows and lay dormant behind workflow_dispatch
T1053 - Scheduled Task/Job T1543 - Create or Modify System Process

The two variants trade immediacy against stealth. SysDiag added a new workflow triggered on push and pull_request, so it collected on the next commit anybody made. Optimize-Build replaced an existing workflow and set its trigger to workflow_dispatch only, which means it never ran on its own and produced no anomalous job history at all. It waited to be invoked later with a stolen token. That choice also sidesteps GitHub's anti-recursion behavior, which suppresses workflow runs triggered by commits that a workflow itself authored: a manual dispatch is not subject to it. A repository owner scanning recent Actions runs for something suspicious would find nothing in the dormant case, because the backdoor's whole design is to have no run history until the attacker wants one.

Variant 1, SysDiag: New workflow on push and pull_request, collects immediately
Variant 2, Optimize-Build: Replaces an existing workflow, workflow_dispatch only, dormant backdoor
Evasion: Manual dispatch sidesteps GitHub's anti-recursion suppression of workflow-authored commits
Detection consequence: The dormant variant has no run history to find until it is used
Where to look instead: Diffs under .github/workflows/, not the Actions run list
workflow_dispatchDormant BackdoorWorkflow InjectionT1053
💻 Execution - Base64 Bash With the Runner's Full Secret Context
05
Each workflow carried a base64-encoded bash payload that executed inside the runner, where every secret the job is entitled to is already present
T1059.004 - Command and Scripting Interpreter: Unix Shell T1027 - Obfuscated Files or Information

Base64 is not sophisticated obfuscation. It is enough, because it defeats a human eye scrolling a YAML diff and most naive keyword greps, and because the reviewer in this scenario is usually nobody. Once the runner executes it, the payload inherits the job's entire context: repository secrets, cloud credentials injected for deployment, the workspace, and the network position of the runner inside whatever environment it was built to reach. A CI runner is a fully privileged machine that happens to be ephemeral, and treating an injected workflow step as anything less than remote code execution with production credentials understates it.

Payload form: Base64-encoded bash embedded in the workflow YAML
Execution context: The runner, holding every secret the job is entitled to
Why base64 suffices: It beats a human skimming a diff and simple keyword scans
Correct framing: Injected workflow step equals RCE with production credentials
Base64 PayloadRunner ExecutionCI RCET1059.004
🔑 Credential Access - Cloud Metadata and Everything in the Environment
06
The payload swept all three clouds' metadata services, CI environment variables, SSH keys, kubeconfigs, Vault tokens and Actions OIDC tokens, plus thirty-plus regex-matched secret patterns
T1552.005 - Cloud Instance Metadata API T1552.007 - Container API

The collection list reads like a checklist of everything a build environment can be persuaded to give up. AWS credentials and IMDSv2 instance role tokens, Google Cloud access tokens and metadata, Azure instance metadata credentials, SSH private keys, Docker and Kubernetes configuration, Vault tokens, GitLab and Bitbucket tokens, and GitHub Actions OIDC tokens, alongside more than thirty regular expressions matching API keys, PEM blocks and database connection strings. Note that IMDSv2 was harvested, not bypassed: IMDSv2 defends against server-side request forgery from a different origin, and it does nothing when the attacker is already executing on the instance. The OIDC token is the highest-value item in the list, because it is the credential that federates into cloud accounts and it is what Mini Shai-Hulud used a week earlier to publish npm packages carrying valid provenance.

Cloud: AWS credentials and IMDSv2 instance role tokens, GCP access tokens and metadata, Azure IMDS credentials
Platform: SSH private keys, Docker and Kubernetes configs, Vault tokens, GitLab and Bitbucket tokens
Federation: GitHub Actions OIDC tokens
Pattern matching: 30+ regexes for API keys, PEM blocks, database connection strings
Note on IMDSv2: Harvested, not bypassed. It stops SSRF from elsewhere, not code running on the host
IMDSv2OIDC Token TheftMulti-Cloud SweepT1552.005
📤 Exfiltration - Straight Out of the Runner to C2
07
Collected secrets were sent to command and control at 216.126.225[.]129 on port 8443
T1041 - Exfiltration Over C2 Channel

Exfiltration went to a single hard-coded endpoint over a port that reads as TLS in a flow log. Hosted GitHub runners have unrestricted outbound network access by default, so nothing stood between the payload and its C2. Compare this with tj-actions/changed-files, which dumped secrets into publicly readable build logs and needed no network path at all: the two campaigns bracket the problem, one exploiting egress and one exploiting log visibility, and both come back to the runner holding secrets it did not need for that long.

C2: 216.126.225[.]129:8443
Path: Direct outbound from the runner; hosted runners allow unrestricted egress by default
Port choice: 8443 reads as ordinary TLS in flow logs
Contrast: tj-actions needed no egress at all, printing secrets into public build logs
C2 ExfiltrationRunner Egress216.126.225.129T1041
💥 Impact - Poisoned Source Republished as an npm Release
08
Poisoned @tiledesk/tiledesk-server versions reached npm between May 19 and 21 without the attacker ever touching the npm account, because the maintainer published from compromised source
T1195.002 - Compromise Software Supply Chain

This is the part that turns a large credential-theft campaign into a supply-chain one, and it is worth being precise about the mechanism. The attacker did not hijack the Tiledesk npm account. They poisoned the GitHub source, and the maintainer then published from it in the ordinary way, which means the malicious releases carried entirely legitimate publisher identity. Every downstream control that verifies who published a package returned a clean answer. In the aftermath npm invalidated granular access tokens that carried write permission while bypassing two-factor authentication, and pushed maintainers toward Trusted Publishing. CISA issued an alert on May 28, 2026 covering this and the related Nx Console compromise.

Package: @tiledesk/tiledesk-server, poisoned versions published May 19 to 21, 2026
Mechanism: Source poisoned on GitHub; the maintainer published normally from it
Consequence: Malicious releases carried legitimate publisher identity
npm response: Invalidated granular write tokens that bypassed 2FA; pushed Trusted Publishing
Advisory: CISA alert of May 28, 2026 covering Nx Console and GitHub repository compromises
npmTiledeskLegitimate PublisherCISA AlertT1195.002

🛡 How to Defend Against This Chain

Protect the branch and require signed commits. Every push in this campaign went in directly under a forged author name. Branch protection with required pull request review stops the direct push; required commit signature verification makes the forged author field worthless, since a signature cannot be faked by typing a different name into git config. Turn both on for default branches everywhere, and make an exception process rather than an exception habit.
Treat .github/workflows/ as protected code with its own reviewers. A workflow change is a change to what executes with your production credentials, so it deserves stricter review than application code, not looser. Use CODEOWNERS to require security review on that path, and alert on any commit touching it from an account that has not contributed before. The dormant workflow_dispatch variant proves the corollary: audit workflow file diffs, not just the Actions run history, because a backdoor that has not run yet appears nowhere in the run list.
Get long-lived cloud credentials out of CI entirely. The payload's value came from what was sitting in the runner. Replace static cloud keys with OIDC federation, scope the trust policy tightly to the specific repository, branch and environment rather than a wildcard, and give the assumed role the shortest workable session. A stolen OIDC token is dangerous in proportion to how loose the trust condition on the other side is, and that condition is yours to write.
Constrain what a runner can reach on the network. Exfiltration went to one hard-coded IP on port 8443 because hosted runners have unrestricted egress. Self-hosted runners in a controlled network with default-deny egress and an allow-list of registries and cloud endpoints turn that step into a failed connection and an alert. Where hosted runners are required, use the platform's network restriction features and monitor for unusual outbound destinations from build infrastructure.
Take developer endpoints as seriously as production hosts. A third of the affected repository owners were on machines with active infostealer infections. That is the campaign's actual root cause, and it is an endpoint management problem: EDR on developer machines, no credential storage in browser profiles, hardware-backed keys and short-lived tokens instead of long-lived PATs, and monitoring of infostealer log dumps for your own domains. Add phishing-resistant MFA on GitHub so a stolen session or password alone is not enough.
Publish from CI with Trusted Publishing, and verify provenance on the way in. Tiledesk's poisoned releases carried valid publisher identity because the source was compromised before the publish, which is exactly why identity checks alone are insufficient. Adopt npm Trusted Publishing and SLSA provenance so releases are attested to a specific workflow run, revoke granular tokens that bypass 2FA, and on the consuming side pin dependencies by hash so a poisoned release cannot enter your builds silently.

Related defense topics