ghtier

Attack paths

Every GitHub attack has two halves: how the attacker gets in, and what they do once they're there. This page lists both, with the setting or practice that stops each step, and ties them back to the IRL incidents where they were used.

The source control part (the first three objectives and most entry vectors) follows François Proulx's attack tree for Boost Security, built on the SLSA source integrity threats. The CI/CD, exposure, persistence and GHES paths are drawn from the incidents.

How they get in

Every technique further down starts from one of these.

Insider

Member with write access

Someone who can already push to the repository. Their reach is exactly what branch protection and rulesets allow; without them, write access means pushing straight to the default branch.

Defense

  • Put the strongest branch protection or ruleset you can on every branch that ships (see Submit malicious code below).
  • Protect release tags with a ruleset, since pushing a tag only needs write access by default.

Repository admin or org owner

Admins can bypass or switch off every control that protects the repository, then turn it back on afterwards. Most of the techniques below get easier or become trivial from here.

Defense

  • Keep the number of org owners and repo admins as small as you can.
  • Manage org and repo settings as code (for example with the GitHub Terraform provider) or through an internal GitHub App, so changes go through review instead of a person's admin rights.
  • Stream the audit log somewhere admins can't edit, and alert on protection rules being changed or deleted.

External, via a stolen identity

Leaked token

A personal access token, npm token, or workflow token found in a dotfile, a CI manifest, a public artifact, a log, or a machine infected by a stealer. A classic PAT with the repo scope is the most valuable find because it works everywhere the owner has access.

Defense

  • Move to fine-grained PATs with the fewest permissions and a short expiry, and require org owner approval for them.
  • Restrict or block classic PATs at the org and enterprise level.
  • Turn on secret scanning with push protection so tokens are caught before they land in a repo.
  • Set persist-credentials: false on checkout, and never upload the workspace as an artifact.

Phished or hijacked account

Password reuse, phishing, a malicious OAuth consent screen, device-code phishing, or a stolen browser session. A session cookie skips 2FA entirely, and an OAuth grant keeps working after the password is changed.

Defense

  • Require 2FA, and push admins and maintainers toward security keys or passkeys. GitHub can't enforce which factor people pick.
  • Put the org behind SAML or OIDC SSO, or use Enterprise Managed Users so the IdP owns the account.
  • Turn on OAuth app access restrictions so a user can't grant an app access to org data on their own.

Compromised GitHub App or OAuth integration

A vendor's integration gets breached, or its private key leaks, and every org that installed it is exposed at whatever permission level it was granted.

Defense

  • Install as few apps as possible, and only from vendors you trust with the permissions they ask for.
  • Review installed apps and OAuth grants regularly and remove the ones nobody uses.
  • Scope installations to specific repositories rather than all repositories.

Stolen SSH or deploy key

A user's SSH key or a deploy key with write access, taken from a laptop or a server. Git over SSH doesn't prompt for 2FA.

Defense

  • Keep keys in an SSH agent with a passphrase, or in hardware or a secure enclave (Secretive on macOS, smart cards, FIDO keys).
  • Require signed commits, so a stolen transport key alone isn't enough to get code merged.
  • Use an SSH certificate authority at the org or enterprise level so keys expire.

External, via the developer endpoint

Compromised developer tool or extension

A backdoored IDE extension, CLI or other local tool runs on a developer's machine and takes whatever that machine can reach: tokens, SSH keys, browser sessions and cloned code. Extensions update themselves, so one bad release reaches every install within minutes.

Defense

  • Allow-list IDE extensions centrally and block the rest.
  • Hold new extension and tool versions for a few days before they install (a cooldown).
  • Keep long-lived tokens off laptops; prefer short-lived, fine-grained ones.
  • Put the organization behind an IP allow list so tokens taken off the machine fail elsewhere.
  • Work on sensitive repositories in Codespaces or dev containers instead of on the laptop.

External, via the pipeline

Untrusted input reaches a privileged workflow

A fork pull request, branch name, issue title or comment is run or interpolated by a workflow that has secrets or a write token. pull_request_target, workflow_run and issue_comment triggers are the usual suspects, along with non-ephemeral self-hosted runners that public PRs can reach.

Defense

  • Don't check out or build PR code in pull_request_target or workflow_run jobs.
  • Pass untrusted values through env variables instead of ${{ }} expressions in run steps.
  • Set the default GITHUB_TOKEN to read-only and grant write per job.
  • Keep self-hosted runners away from public repos, and make them ephemeral.
  • Lint workflows with zizmor or a similar tool in CI.

Compromised Action or package

A third-party Action or npm/PyPI package you depend on is backdoored upstream. It runs with whatever your workflow or developer machine has in scope.

Defense

  • Pin third-party Actions to a full commit SHA, not a tag.
  • Review new dependencies in PRs and hold new package versions for a few days before they're allowed in (a cooldown).
  • Disable install scripts where you can (npm ignore-scripts).

Prompt injection into an AI agent

An agent holding a GitHub token reads attacker-written text (an issue, a PR description, a file) and follows it, using the token for things its user never asked for.

Defense

  • Give agents a token scoped to the single repository they're working on.
  • Don't let an agent read public input and write to private repos in the same session.
  • Require a human to approve anything an agent pushes or posts.

External, via the platform

Vulnerability in GitHub Enterprise Server

A bug in a self-hosted GHES instance: an authentication bypass, or a way to turn a low-privilege role or plain push access into root on the appliance.

Defense

  • Patch GHES quickly; several of these were critical and exploitable before login.
  • Keep the Management Console and admin SSH off the general network.

Submit malicious code

Get attacker-controlled code onto a branch that gets built and shipped.

  1. Push directly, without review

    Attack

    There's no branch protection on the target branch, or it doesn't require a review.

    Defense

    Require a pull request with approvals, status checks and signed commits, and don't let admins bypass it. Leave force pushes and deletions off.

  2. Approve your own PR from a second account

    Attack

    The attacker talks an owner into granting write access to one or more extra accounts, then opens a PR from one and approves it from another before anyone notices.

    Defense

    Tie every membership to one person through SSO or Enterprise Managed Users.

  3. Have a workflow approve the PR

    Attack

    Orgs created before 2022 let GITHUB_TOKEN approve pull requests by default. The attacker adds a workflow to their PR that approves it as soon as it opens.

    Defense

    Turn off "Allow GitHub Actions to create and approve pull requests" in the org Actions settings.

  4. Push more commits after approval

    Attack

    The attacker gets a clean change approved, then pushes the bad code to the same PR and keeps the old approval.

    Defense

    Dismiss stale pull request approvals when new commits are pushed.

  5. Approve someone else's PR, then push to it

    Attack

    The attacker approves a colleague's good PR, pushes their own bad commit on top, and the colleague's approval or their own covers it.

    Defense

    Require approval of the most recent reviewable push.

  6. Hide it in a change nobody can review

    No setting fully fixes this

    Attack

    The bad code is buried in a PR touching thousands of lines, or arrives as a new dependency the attacker publishes and controls.

    Defense

    Keep PRs small, require CODEOWNERS review on lockfiles and manifests, and check every new dependency before it's added.

  7. Use a trusted colleague's account

    No setting fully fixes this

    Attack

    Same as a sock puppet, but with a real senior engineer's account. Reviewers question their changes less.

    Defense

    The entry-vector controls above (SSO, strong 2FA, short-lived tokens). There's no single fix.

  8. Trick or outlast the reviewer

    No setting fully fixes this

    Attack

    The change uses a bug class the reviewer doesn't know, is written to be confusing, or goes to the reviewer known to approve quickly.

    Defense

    Require two reviewers on sensitive paths and run SAST, knowing a careful attacker will test against it first. Audit who approves unusually fast.

  9. Bypass or switch off the rules

    Attack

    An admin merges past branch protection, or turns it off for a few minutes and back on.

    Defense

    Fewer admins, settings as code, and audit log alerts on protection changes.

Delete code or cover tracks

Remove the malicious commit, the branch it came from, or the workflow runs that show what happened.

  1. Force push or delete a protected branch

    Attack

    With a weak rule, or as an admin who loosens it for a moment, the attacker rewrites history on the branch.

    Defense

    Never enable Allow force pushes or Allow deletions on protected branches. Both are off by default.

  2. Delete branches and workflow runs

    Attack

    After using a stolen token, the attacker deletes the branches and workflow runs they created so the logs are gone.

    Defense

    Stream the audit log out of GitHub. Workflow runs can be deleted from the repo, but the audit log entries for them stay put.

Point a release tag at a bad commit

Ship code that never went through review by moving the tag a build or a downstream user trusts.

  1. Push a release tag to an unreviewed commit

    Attack

    The release pipeline builds on tags like v1.2.3. Pushing a tag only needs write access, so the attacker tags a commit on an unprotected branch, or an old commit with a known bug.

    Defense

    Add a tag ruleset that restricts who can create, update or delete release tags. The older tag protection rules have been folded into rulesets.

  2. Repoint existing tags on a published Action

    Attack

    With write access to a widely used Action, the attacker force-moves existing version tags to a malicious commit, so everyone pinning by tag pulls it on the next run.

    Defense

    Maintainers: block tag updates and deletions with a ruleset. Consumers: pin Actions to a full commit SHA.

Steal CI secrets and tokens

Turn one foothold into credentials for package registries, cloud accounts and other repos.

  1. Add a workflow that dumps secrets

    Attack

    With a token that has the workflow scope, the attacker commits a workflow that sends ${{ toJSON(secrets) }} or named secrets to their own server.

    Defense

    Protect .github/workflows with CODEOWNERS and a ruleset, keep workflow scope out of PATs, and move secrets into environments with required reviewers.

  2. Run PR code in a privileged workflow

    Attack

    A pull_request_target or workflow_run job checks out the attacker's PR, or interpolates its title into a shell step, and the attacker's code runs with secrets in scope.

    Defense

    Treat anything from a PR as untrusted, pass it through env variables, and keep secrets out of those jobs.

  3. Read secrets from the runner

    Attack

    A backdoored Action or a process on a reused runner reads secrets out of runner memory or from .git/config, and prints them to logs or uploads them as artifacts.

    Defense

    Pin Actions by SHA, use ephemeral runners, set persist-credentials: false, and don't upload the checkout directory as an artifact.

  4. Steal a session and read secrets from a CI vendor

    Attack

    Malware on an engineer's laptop takes a 2FA-backed session cookie and uses it to pull customer secrets out of a CI service.

    Defense

    Use OIDC federation instead of long-lived cloud keys stored in CI, and rotate secrets on a schedule.

Poison the build or published package

Change what gets released without touching the reviewed source.

  1. Poison a shared Actions cache

    Attack

    A PR workflow writes a cache entry that the release workflow later restores, so attacker-controlled files end up in the release build.

    Defense

    Don't share caches between untrusted PR jobs and release jobs, and don't restore caches in release workflows at all.

  2. Trigger the publish workflow from a tampered branch

    Attack

    With a write token, the attacker creates a branch with a modified CI script and starts the publish workflow against it through workflow_dispatch.

    Defense

    Lock publish workflows to protected branches or tags with deployment environments, and publish with OIDC trusted publishing instead of a stored registry token.

  3. Backdoor a persistent self-hosted runner

    Attack

    Code from a fork PR runs on a self-hosted runner that isn't wiped afterwards, and stays there for the next release job.

    Defense

    Use ephemeral self-hosted runners, and don't attach them to public repositories.

  4. Publish a malicious version with a stolen token

    Attack

    A stolen npm or PyPI token is used to publish a new version with an install script that steals the next victim's tokens.

    Defense

    Publish only from CI with trusted publishing, behind an environment with required reviewers, and turn on 2FA for the registry account. Provenance shows which commit was built, not that anyone approved it, so the branch and tags the workflow builds from need protecting too.

Read or expose private code

Copy private repositories out, or make them public.

  1. Flip private repos to public

    Attack

    An owner or admin token is used to change visibility on many repos at once.

    Defense

    Restrict repository visibility changes to org owners, keep owner accounts few, and alert on visibility changes in the audit log.

  2. Get an agent to copy private code into public

    Attack

    A public issue tells an AI agent to read a private repo and post what it finds somewhere public.

    Defense

    Scope agent tokens to one repository and require approval before an agent writes anywhere.

Keep access

Make sure revoking the first stolen credential doesn't end it.

  1. Register the attacker's machine as a runner

    Attack

    The infected host registers itself as a self-hosted runner and plants a workflow that runs commands on it when triggered.

    Defense

    Limit who can register runners, use runner groups, and review the runner list regularly.

  2. Leave a dormant workflow behind

    Attack

    The attacker replaces a workflow with one that only runs on workflow_dispatch. Nothing happens until they trigger it through the API, long after the push that planted it has scrolled out of view.

    Defense

    Require code owner review for .github/workflows, and alert on new or changed workflows and on workflow_dispatch runs nobody expected.

Take over the GHES appliance

Go beyond GitHub's permission model to the server it runs on.

  1. Forge a login

    Attack

    A SAML signature bug lets an unauthenticated attacker sign in as any user, including a site admin.

    Defense

    Patch GHES, and prefer encrypted assertions where the IdP supports them.

  2. Escalate from a GHES role to root

    Attack

    A Management Console editor, an enterprise admin editing pre-receive hooks, or even a user with push access abuses a bug to run commands as root.

    Defense

    Patch GHES, and restrict Management Console and pre-receive hook access to as few people as possible.

Escalations

How one permission becomes more. Each step starts from a permission (or from code already running somewhere it should not) and ends with access the attacker did not have before. These are the steps to break first: remove the starting permission, or add the control that stops the step.

Change a workflow, get its secrets

Commit a workflow, or change an existing one, that runs on push and sends the repository's secrets out. A workflow can also ask for any token permission, including id-token: write.

Gains Every secret the repository's workflows can read, and cloud access through OIDC.

Move release tags on a widely used Action

Force-move existing version tags on an Action's repository to a malicious commit.

Gains Code execution, with their secrets, in every repository that pins the Action by tag.

Start a privileged publish job by hand

Run the publish workflow with workflow_dispatch against a branch whose build script was changed.

Gains Whatever the publish job holds: package registry tokens and the right to publish.

Equivalent to:T1Packages: Write

Flip private repositories to public

Change repository visibility, one repository at a time or in bulk with an owner token.

Gains Private source code exposed to everyone.

Register a self-hosted runner

Create a runner registration token and register a machine the attacker controls.

Gains Every job routed to that runner, with the secrets those jobs receive, and a foothold that outlives the stolen token.

Add a collaborator or guest that outlives the attack

Add an outside collaborator, a guest, a deploy key or an App grant under the attacker's control.

Gains Access that survives password resets and token revocation.

Equivalent to:T2Contents: Write

Add yourself to a team with repository access

From: The Team maintainer role on a team with repository access

A team maintainer adds an account they control to their team.

Gains Every repository role the team holds, often Write or Admin on production repositories.

Change a shared reusable workflow

Push to the repository that holds a reusable workflow other repositories call by branch or tag. Callers that pass secrets: inherit hand it every secret they have.

Gains Code running in every calling repository's workflow, with the secrets and GITHUB_TOKEN those runs get: an internal version of a compromised Action.

Equivalent to:T1Workflows: Write

Harvest leaked secrets from scanning alerts

List the secret scanning alerts. Each one includes the leaked value, and many are still valid.

Gains Live credentials found in code and history: cloud keys, tokens and passwords, often for systems outside GitHub.

Join any organization as an enterprise owner

From: The Enterprise owner role

An enterprise owner can join any organization owned by the enterprise as a member or as an organization owner.

Gains Organization owner in every organization of the enterprise, even ones they were never added to.

Claim the old name of a renamed Action

From: An Action or reusable workflow whose owner renamed or deleted their account or repository

Register the freed owner or repository name and publish code at the same path. Workflows that still use the old owner/repo@ref fetch the new owner's code.

Gains Code running in every workflow that still uses the old path, with its secrets and GITHUB_TOKEN.

Equivalent to:T1Workflows: Write

Take the workflow token from a runner

From: Code already running in a workflow job: a compromised Action, dependency or pull request

Read GITHUB_TOKEN from runner memory or from .git/config, where actions/checkout leaves it unless told not to.

Gains The job's token, often with write access to the repository.

Equivalent to:T2Contents: Write

Poison a cache the release build restores

From: A pull request from a fork whose workflow can write to the shared Actions cache

Write a cache entry under a key the release workflow later restores.

Gains Attacker files inside the release build, and with them the release job's credentials.

Equivalent to:T1Packages: Write

Persist on a non-ephemeral self-hosted runner

From: A pull request from a fork that runs on a self-hosted runner that is not wiped between jobs

Leave a process or file behind that the next job, often a release, picks up.

Gains The next jobs on that runner, with their secrets and tokens.

Equivalent to:T2Contents: Write

Incidents by path

Each incident, newest first, with the way in and what the attacker went after.

IncidentWay inWent after
ChainDrop: a hijacked maintainer account publishes through trusted publishing
OrganizationAugust 2026
GitHub's internal repositories taken through a VS Code extension
UserMay 2026
Actions cache poisoning (Angular, TanStack)
OrganizationOngoing
Megalodon: mass backdooring of CI workflows
OrganizationMay 2026
TeamPCP tag force-overwrite campaign
OrganizationMarch – April 2026
prt-scan: automated pull_request_target attacks
OrganizationMarch – April 2026
CVE-2026-3854: push-option metadata RCE
EnterpriseFixed 2026
hackerbot-claw autonomous agent RCE
OrganizationFebruary 2026
GlassWorm: editor extensions that steal GitHub tokens and force-push
UserOctober 2025 – March 2026
CVE-2025-11578: pre-receive hook symlink escape
EnterpriseFixed 2025
GhostAction
OrganizationSeptember 2025, returned 2026
Shai-Hulud self-propagating worm (1.0 & 2.0)
OrganizationSeptember 2025 / November 2025 / May 2026
Nx "s1ngularity"
OrganizationAugust 2025
Toptal repo-visibility flip and wiper
OrganizationJuly 2025
GitHub MCP / agentic token breadth
ApplicationMay 2025 – ongoing
CVE-2025-3509: pre-receive hook RCE via port reuse
EnterpriseFixed 2025
tj-actions/changed-files supply-chain compromise (CVE-2025-30066)
ApplicationMarch 2025
Salesloft Drift GitHub → AWS pivot
OrganizationMarch – June 2025
OAuth consent phishing at scale
User2025–2026
Ultralytics build cache poisoning
OrganizationDecember 2024
ArtiPACKED artifact token leakage
ApplicationAugust 2024
CVE-2024-4985: SAML auth bypass
EnterpriseFixed 2024
CVE-2024-1374: Management Console template injection
EnterpriseFixed 2024
Mercedes-Benz source code exposure
UserJanuary 2024
Dependabot impersonation
UserJuly 2023
Self-hosted runner takeovers (PyTorch and others)
Organization2023–2024
CircleCI session-cookie theft
UserDecember 2022 – January 2023
Heroku / Travis CI OAuth token theft
ApplicationApril 2022
GitHub App installation token abuse
ApplicationOngoing
CVE-2022-23732: Management Console path traversal / CSRF bypass
EnterpriseFixed 2022
"Pwn requests": pull_request_target & workflow_run
OrganizationOngoing

Sources