Back

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.

Published 11 November 2026 · Gulger Mallik · All times UTC

Status
Resolved
Severity
High
Attacker active
25 Oct to 11 Nov 2026
Contained
11 Nov 2026, 11:14 UTC
Repositories touched
64 (none merged)
Secrets exposed
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.

Current status: resolved. The stolen token is revoked on GitHub and removed from the server. Exposed secrets are rotated and no longer work. Every attacker branch, workflow and run is removed. None of the decoy pull requests was merged. The responsible parties have been told.

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

Attacker activityResponse
  1. 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.

  2. 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.

  3. 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.

  4. 29 Oct, ~06:30–07:00 (attacker activity)

    Second probe wave across the same repositories. More commit pairs. The malicious workflow runs twice more.

  5. 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.

  6. 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.

  7. 11 Nov, ~10:09 (response)

    Suspicious activity is spotted. The investigation starts and evidence is collected across every affected repository.

  8. 11 Nov, 10:54 (response)

    The malicious workflow and its run logs are deleted. The exfiltration path is closed.

  9. 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.

  10. 11 Nov, 11:14 (response)

    A verification scan finds no remaining attacker branches, workflows or runs. The incident is contained.

  11. 11 Nov (response)

    The parties responsible for the compromised server and the affected repositories are notified.

Time from first observed attacker activity to containment: about 17 days. From detection to containment: about an hour.

Root cause

Primary cause: a third-party server was compromised through a separate codebase deployed on it. That server stored a long-lived GitHub personal access token with broad repository and workflow permissions. Whoever controlled the server could act as my account.

Contributing factors

FactorEffect
The token had broad scopeIt could write to every repository the account could reach, including workflow files, not just the one repository the server needed.
The token never expiredIt stayed usable for the whole 17 days.
Default branches weren’t protectedCommits and workflow files went straight to the default branch with no pull request or review.
Any workflow could read secretsThe planted workflow could read deployment secrets, and encoding them got past GitHub’s log masking.
Workflows ran on any pushPushing a branch was enough to run code.
No alerts on account activityProbes 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.

This is a sanitised public summary. Repository, organisation and client names, secret names and internal evidence are left out on purpose. If you think you were affected, please get in touch through the contact page.