Incident Report: GitHub Access Token Compromise (11 Nov 2026)
A GitHub access token stored on a third-party server was stolen after the server was compromised through a separate codebase. This report covers what happened, the timeline, the root cause, what was done and what has changed since.
- Resolved
- High
- 25 Oct to 11 Nov 2026
- 11 Nov 2026, 11:14 UTC
- 64 (none merged)
- 1 repository (rotated)
Summary
A server run by a third party was compromised through a separate codebase hosted on it. That server held a GitHub personal access token for my account, and the attacker used it. For about 17 days they had write access to the repositories the account could reach, across several organisations.
They probed repositories for write access, pushed small commits to default branches and added GitHub Actions workflows. In one repository, a planted workflow leaked deployment secrets. They also pushed decoy pull requests to 64 repositories to trigger more workflows.
What happened
Initial access
The attacker got into a server run by a third party through a different codebase deployed on it. A GitHub personal access token with repository and workflow permissions was stored on the same server. Every malicious action was done as my account with that token. There’s no sign of a vulnerability in GitHub itself.
Reconnaissance
Twice, a randomly named branch was created, pushed and deleted within about ten seconds in 53 repositories. This is a quiet way to find out which repositories a token can write to.
Direct commits to default branches
Pairs of commits titled “ci: refresh build cache” and “ci: restore” were pushed straight to the default branch of five repositories. Each pair added a change and then reverted it. This most likely triggered CI where secrets are available, without changing the final code.
Secret exfiltration
In one repository, a workflow named “exfil” printed the repository’s deployment secrets to the job log. The values were base64-encoded and reversed first, so GitHub’s automatic masking didn’t catch them. It ran successfully nine times.
Decoy pull requests
A branch with a three-line “dependency audit notes” file was pushed to 64 repositories, each with a matching pull request. The pull request was camouflage. The push was the point, because it fires any workflow set to run on push.
Fake audit workflows
Workflows called “Dependency Audit” were added in four repositories. All of their runs were cancelled before the payload step, so nothing ran.
Timeline
Before 25 Oct (attacker activity)
A server run by a third party is compromised through a separate codebase hosted on it. A GitHub access token stored on that server is taken.
25 Oct, 17:41–19:40 (attacker activity)
First probe wave: short-lived branches are created and deleted within seconds in 53 repositories to test write access. First small commit pairs are pushed straight to the default branch of a few repositories.
27 Oct, 06:23 (attacker activity)
A malicious GitHub Actions workflow is committed to one repository. It runs and leaks that repository's deployment secrets.
29 Oct, ~06:30–07:00 (attacker activity)
Second probe wave across the same repositories. More commit pairs. The malicious workflow runs twice more.
10 Nov, 13:20–15:03 (attacker activity)
Fake “Dependency Audit” workflows are added in four repositories. Every run is cancelled before it does anything.
10 Nov 15:03 → 11 Nov 08:51 (attacker activity)
A decoy branch and pull request (“docs: dependency audit notes”) are pushed to 64 repositories to trigger push-based workflows. The malicious workflow runs six more times. The last attacker action is at 08:51.
11 Nov, ~10:09 (response)
Suspicious activity is spotted. The investigation starts and evidence is collected across every affected repository.
11 Nov, 10:54 (response)
The malicious workflow and its run logs are deleted. The exfiltration path is closed.
11 Nov, late morning (response)
Cleanup applied: decoy branches deleted, pull requests closed, fake workflows disabled. Exposed secrets rotated. The stolen token is revoked on GitHub and removed from the server.
11 Nov, 11:14 (response)
A verification scan finds no remaining attacker branches, workflows or runs. The incident is contained.
11 Nov (response)
The parties responsible for the compromised server and the affected repositories are notified.
Root cause
Contributing factors
| Factor | Effect |
|---|---|
| The token had broad scope | It could write to every repository the account could reach, including workflow files, not just the one repository the server needed. |
| The token never expired | It stayed usable for the whole 17 days. |
| Default branches weren’t protected | Commits and workflow files went straight to the default branch with no pull request or review. |
| Any workflow could read secrets | The planted workflow could read deployment secrets, and encoding them got past GitHub’s log masking. |
| Workflows ran on any push | Pushing a branch was enough to run code. |
| No alerts on account activity | Probes across many repositories went unnoticed for about 17 days. |
Impact
Confirmed
- Deployment secrets from one repository were exposed. They have been rotated and the old values don’t work.
- Unauthorised commits, branches, workflows and pull requests across up to 64 repositories. All removed or closed.
Verified not affected
- No pull request was merged, and no lasting code changes remain.
- No new SSH or GPG keys, gists or account email changes.
- No new collaborators, invitations or organisation members.
- No private repositories made public.
- No organisation-level secrets exposed.
Actions taken
Containment
- Deleted the malicious workflow and all of its run logs, so the leaked values are no longer in the logs.
- Disabled every attacker-added workflow.
Eradication
- Deleted every decoy and probe branch (64 decoy branches, 0 failures).
- Closed all 64 decoy pull requests. None had been merged.
- Checked the default-branch commit pairs for any leftover changes.
Credential recovery
- Revoked the stolen token on GitHub and removed it from the compromised server.
- Rotated the exposed deployment secrets. The old values no longer work.
- Rotated keys for other potentially exposed projects as a precaution.
Verification and notification
- Ran a full verification scan: no attacker branches, workflows or runs remain.
- Audited the account. No new SSH or GPG keys, gists, collaborators, invitations, email changes, webhooks, deploy keys or visibility changes.
- Notified the parties responsible for the compromised server and the owners of the affected repositories.
Preventions
In place
- The stolen token is revoked everywhere, and the exposed secrets are rotated.
- No personal access token is kept on any third-party or client-managed server.
- A repeatable scan script checks every repository for unexpected branches, workflows and direct pushes to the default branch.
- A baseline of known-good keys, webhooks, deploy keys and collaborators is recorded, so changes stand out.
Being rolled out
- Servers get repository-scoped deploy keys or fine-grained tokens with the fewest permissions, an expiry date and no workflow scope.
- Branch protection on default branches: pull requests required, force-pushes blocked.
- Required review for changes to workflow files (CODEOWNERS on .github/workflows).
- Deployment secrets moved into protected environments with required reviewers.
- The default GITHUB_TOKEN set to read-only, and push triggers limited to protected branches.
- Secret scanning, push protection and alerts on unusual account activity.
- Strong two-factor authentication (passkeys or security keys) required on the account and in every organisation.
Lessons learned
A credential is only as safe as the weakest place it’s stored
The breach came through a separate codebase on a shared server, not through GitHub. Any secret on a host shares that host’s risk.
Scope and expiry limit the damage
A token limited to one repository with read-only access would have turned an account-wide incident into a minor one.
CI/CD is part of the attack surface
Workflows that run on any push and can read secrets let anyone with write access run code. Protect workflows like production code.
Detection matters as much as prevention
The attacker was noisy: dozens of repositories, within minutes. Basic alerts would have cut 17 days down to hours.