Baseline Network Monitoring Catches What Signatures Miss

▶ Watch (08:54)

Step Security’s detection didn’t come from a signature or a known-bad IP list. It came from a network baseline built over 2,000 runs of the same workflow. Every run called github.com, amazonaws.com, and hashicorp.com. On March 14th, a new destination appeared: 0.0.githubusercontent.com. That single deviation triggered a Slack alert within hours of the compromise going live.

Tracing the call led to a curl process inside the tj-actions/changed-files step, downloading a file named memdump.py. Mutable release tags had been redirected just 3 hours earlier. The baseline caught what no signature would have.

Impostor Commits: Mutable Tags as an Attack Surface

▶ Watch (12:39)

GitHub release tags are mutable. A tag like v4 is a pointer that anyone with write access can move to any commit, including one that lives only in a fork. Those are impostor commits: accessible via GitHub’s API from the original repo, but absent from every branch and commit graph in it.

All tags for tj-actions/changed-files were redirected to a single malicious fork commit 3 hours before detection. Every workflow referencing v35, v44, or any other tag automatically ran the attacker’s code. No pull request. No code review. Nothing in the commit history signaled any change.

How the Exploit Stole CI/CD Secrets from Memory

▶ Watch (15:05)

The impostor commit decoded a base64 string and ran it as a shell script that downloaded memdump.py from a public GitHub Gist. memdump.py hunted for the runner.worker process and dumped its entire memory. That process is where GitHub stores CI/CD secrets before passing them to workflow steps.

The dump was searched for GitHub’s known secret storage format, then printed to stdout double-base64-encoded. Single encoding is masked by GitHub’s log redaction. Double encoding is not. In the end-to-end breach demo showing secrets in build logs (18:06), decoding the output shows an AWS access key and GitHub token in plain text.

The Attack Chain: From spotbugs to 23,000 Repositories

▶ Watch (21:25)

The tj-actions breach didn’t start at tj-actions. A pull-request design flaw in spotbugs/sonarfindbugs let an attacker steal a personal access token. That token compromised spotbugs/spotbugs, which gave write access to the reviewdog GitHub organization. The reviewdog/action-setup v1 tag was redirected to an impostor commit. When tj-actions/changed-files ran its own CI workflow against that action, the dumped runner.worker memory leaked the tj-actions PAT. The attacker used it to redirect all tj-actions/changed-files tags.

Four hops from one pull-request vulnerability. The end result: over 23,000 public repositories at GitHub, Meta, Microsoft, and Hugging Face running the malicious code.

Why the Attack Stayed Hidden

▶ Watch (29:03)

The attackers picked each evasion technique deliberately. The tj-actions impostor commit downloaded memdump.py from gist.githubusercontent.com, a GitHub-owned domain that EDR agents and runtime security tools whitelist without inspection. The reviewdog impostor commit had the exploit embedded directly, so it made zero outbound network calls. Impostor commits don’t appear in any branch or commit history, so the repository looked clean throughout the attack.

The attacker also impersonated renovate-bot in the commit metadata. Nothing in the standard toolchain would flag any of it.

Four Controls That Limit the Blast Radius

▶ Watch (32:41)

CI/CD runners have zero security monitoring in most organizations. Wazuh, Falco, or Tetragon can capture outbound network calls and process events from runners, then baseline that data per pipeline. Organizations that had pinned tj-actions/changed-files to a commit SHA instead of a version tag were unaffected when the tags were redirected.

An allowlist limits which of the 25,000 marketplace actions your organization can run. An IR plan, written before a compromise, defines how to find affected workflow runs, spot leaked credentials in build logs, and rotate persistent tokens. Without one, that work is manual. Here, credentials sat exposed for close to 24 hours.

Q&A

What role does commit signing play in preventing attacks like this? Commit signing helps at review time, but once a workflow already trusts a tag, GitHub cannot verify at runtime whether the commit that tag now points to is signed, so a tag swap to an unsigned impostor commit still executes. ▶ 39:02

Key Takeaways

  • GitHub tags are mutable; pinning third-party actions to an immutable commit SHA prevents tag-swap attacks.
  • memdump.py targets the runner.worker process; double base64 encoding hides stolen secrets in plain-text build logs.
  • A pull-request design flaw in spotbugs started a four-hop chain that compromised 23,000+ public repositories in hours.