Skip to content

This tool is not affiliated with, endorsed by or sponsored by GitHub, Inc. or Microsoft Corporation. GitHub and GitHub Actions are trademarks of GitHub, Inc. Other names are trademarks of their respective owners.

Leaked GitHub token: what to do and how to investigate

A GitHub PAT or OAuth token leaked? Revoke it, find its hashed_token in the audit log, check new IPs, countries and clones, then scope what the token reached.

Published on 6 min read

TL;DR. Treat the token as used until the logs show otherwise. Revoke it (anyone holding the literal value can do it through the credential revocation API). Hash it (echo -n TOKEN | openssl dgst -sha256 -binary | base64) and search the audit log for hashed_token:"…". Then export Git events, which UI and API searches leave out. Look for new countries and IPs, clone bursts and anything the token created: keys, workflows, apps, runners. Finally, check the owner's machine for an infostealer.

Personal access tokens are the credential attackers most often reuse against GitHub. A developer's ghp_… in a .env file, a CI log, a pasted shell history or an infostealer dump gives an attacker that developer's access to every organization the token is authorized for, with no password and no 2FA prompt. This playbook covers the investigation, not just the rotation.

First hour: contain without destroying evidence

  1. Export first, revoke second, if you can do both within minutes. Revocation is itself logged, so it destroys nothing. But once the token is dead, the attacker may switch to whatever persistence they planted, and you want the "before" picture. If the token is actively being abused, revoke immediately.
  2. Revoke. The token's owner can delete it under Settings → Developer settings → Personal access tokens. With the literal value, anyone can call the revocation endpoint, which covers classic and fine-grained PATs, OAuth app tokens and GitHub App user tokens. In organizations with SAML SSO, an owner can also revoke the token's SSO authorization.
  3. Assume the source is still compromised. If the token came from a developer workstation, an infostealer probably took browser cookies, SSH keys and cloud CLI credentials as well. Re-imaging comes before re-issuing.

GitHub already revokes some tokens for you. Per its token revocation documentation, a valid OAuth, GitHub App or personal access token pushed to a public repository or public gist is revoked automatically. So is an OAuth or personal access token that has not been used for a year. Nothing does this for a token leaked in a private repository, a build log or a paste site.

Find the token in the audit log

Token-authenticated events carry three fields, documented in identifying audit log events performed by an access token:

FieldMeaning
hashed_tokenBase64 SHA-256 of the token value
programmatic_access_typee.g. "Personal access token (classic)", "Fine-grained personal access token", OAuth, GitHub App
token_scopesScopes of a classic token, e.g. repo, workflow, read:org

Compute the hash of the leaked value and search for it:

echo -n "$LEAKED_TOKEN" | openssl dgst -sha256 -binary | base64
# then, in the audit log search box:
# hashed_token:"Xkr8WMW4...="

Two caveats. UI and REST searches exclude Git events, so you must export them to see clones made with the token. And if you do not have the value (only a suspicion), work the other way round. On Enterprise Cloud, the credential inventory export lists hashed_token values with their owners (except fine-grained PATs), so you can tie an unknown hash seen in the log to a person. The hashed token glossary entry has more detail.

Was the token used by someone else?

A stolen token is replayed next to its legitimate owner, so you compare the token with itself:

  • New country for the token. The analyzer's token-new-country rule (high) fires when a hashed_token shows up from a country never seen for it, once the token has at least 24 hours of history in the export. Replays from a VPS abroad fit this pattern.
  • New IP for the token. token-new-ip (low) is noisy on its own, because laptops move between networks. It matters when it shares a token with another finding.
  • User agent change. A developer's git/2.39.5 (Apple Git-154) suddenly paired with curl/8.5.0 or python-requests on the same hash.
  • Time of day. Activity at 02:00 in the owner's time zone, several days in a row, is worth a question.
  • Volume. A git.clone burst across many repositories (see repository exfiltration).

These detections need actor_ip and country data. If the export has no IPs, enable IP display, which also applies to existing events, and export again.

What did the token do?

List every event carrying the hash (in the analyzer, use Entities → Tokens → Show events) and sort the actions into three groups:

GroupActionsWhat it means
Read / exfiltrationgit.clone, git.fetch, repo.download_zipCode (and every secret in its history) is out
Write / tamperinggit.push, workflows.created_workflow_run on a new branch, protected_branch.destroyCode or pipelines changed. See Actions secrets leak
Persistencepublic_key.create, hook.create, integration_installation.create, org.register_self_hosted_runner, personal_access_token.request_createdAccess that survives revocation

The token's scopes set the limits. A classic token with repo and workflow can push workflow files. With admin:org it can change organization settings. A fine-grained token only reaches the repositories and permissions it was granted, and in organizations that require approval, personal_access_token.request_created and .access_granted events show when it got that access.

Remove what the token left behind

Revocation does not undo what the attacker created. Before you close the incident:

  • Delete unknown deploy keys and SSH keys (public_key.create). A write-enabled deploy key keeps push access after every password and token reset.
  • Remove webhooks, GitHub Apps, OAuth app approvals and self-hosted runners added in the window.
  • Delete attacker branches and review .github/workflows on every branch, not only the default one.
  • Rotate the Actions secrets readable by any workflow the token could push, then the cloud keys among them (see the cloud pivot).
  • Look for other tokens of the same owner used from the attacker's IP. Pivot on the IP, not only on the hash.

Prevent the next one

  • Set an organization personal access token policy: restrict classic tokens, enforce a maximum lifetime, require approval for fine-grained tokens. The analyzer flags when that policy is weakened (pat-policy-weakened).
  • On Enterprise Cloud, an IP allow list also applies to personal access tokens and SSH keys. Stolen tokens then only work from your networks.
  • Prefer GitHub Apps with short-lived installation tokens over long-lived PATs for automation.

FAQ

Does GitHub revoke leaked tokens automatically?

GitHub automatically revokes a valid OAuth token, GitHub App token or personal access token pushed to a public repository or public gist. Tokens leaked elsewhere (a private repository, a CI log, a chat, an infostealer dump) are not revoked for you.

How do I find a token's events in the audit log?

Compute its hash with echo -n TOKEN | openssl dgst -sha256 -binary | base64, then search the audit log for hashed_token:"VALUE". Note that UI and API searches exclude Git events. Export them to check clones.

Is revoking the token enough?

No. Revocation stops future use but not what the attacker already did or planted: SSH and deploy keys, OAuth apps, workflows on new branches, runners and webhooks survive it. Scope the token's activity, remove persistence and rotate secrets its workflows could read.

Related articles

What the GitHub audit log does not record: 180-day and 7-day retention, plan limits for API and Git events, hidden IPs, no file contents or secret reads.
GitHub organization takeover indicators in the audit log: new owners, 2FA requirement off, SAML SSO changed, IP allow list off, audit streaming removed.
Find source code theft in the GitHub audit log: git.clone bursts, ZIP archive downloads, repos made public or transferred, forks, and what each leaves behind.

This tool is not affiliated with, endorsed by or sponsored by GitHub, Inc. or Microsoft Corporation. GitHub and GitHub Actions are trademarks of GitHub, Inc. Other names are trademarks of their respective owners.