Back to Articles
Security
Aug 10, 202618 min read

How My GitHub Was Silently Backdoored for 15 Months — And How I Found and Killed It

I discovered a sophisticated C2 backdoor quietly living inside my GitHub repos for over a year. It used the Ethereum blockchain to hide its server IP, triggered on every build, and left almost no traces. Here is exactly how it worked, how I found it, and how I wiped it from history.

How My GitHub Was Silently Backdoored for 15 Months — And How I Found and Killed It

My laptop died. When I restored from GitHub, I noticed something wrong — Vercel was sending me build notifications for commits I never made. I started investigating and found a backdoor that had been living quietly inside my repositories for over 15 months. It used the Ethereum blockchain to hide its command server, triggered automatically on every npm build, and was designed to be invisible to a casual code review. This is the full story of what happened, how it worked, and how I cleaned it from every commit in history.

How It Started: The Notification That Should Not Have Existed

After restoring my work from GitHub, I started getting Vercel build success and failure emails for deployments I had no memory of triggering. At first I assumed it was cached pipeline runs or some Vercel background job. But when I checked the commit timestamps, the pushes had happened at times I was not at my computer — and the commit messages were legitimate-sounding feature descriptions I had never written.

Something or someone had pushed to my repos. And they had done it carefully enough to not immediately look suspicious.

The Investigation: Scanning All Repositories

I did a full audit of my GitHub account — every repository, every commit, every file change. The first thing I noticed was a mass event on a single day: over 60 repositories, including ones I collaborate on, all received push events within a 13-minute window. Every single one of those pushes had zero commits attached — they touched the branch refs but changed no code. Someone had run a script against my token.

Then I found the actual infection. Inside a config file that Next.js projects load automatically on every build — postcss.config.mjs — there was malware. Not a separate suspicious file, not an obvious injection. It was appended directly after the legitimate export, hidden behind hundreds of spaces so it would not appear in a normal file view without horizontal scrolling.

The Malware: A C2 Backdoor That Hides Its Server Inside the Ethereum Blockchain

This was not a simple script-kiddie injection. The malware was a sophisticated, multi-stage command-and-control backdoor. Here is exactly how it worked:

  • Stage 1 — Blockchain IP Lookup: The malware queried multiple Ethereum RPC endpoints (public nodes like 1rpc.io, drpc.org, publicnode.com) to find a specific transaction from a hardcoded wallet address. The destination address of that transaction, when decoded as raw bytes, contained the attacker's C2 server IP. This is clever — the IP is never hardcoded in the malware itself. The attacker can rotate servers by simply making a new ETH transaction.
  • Stage 2 — Code Download: Once the C2 IP was resolved, the malware made an HTTP request to fetch an arbitrary payload. The payload was XOR-encrypted with a hardcoded key to avoid detection by static content scanners.
  • Stage 3 — Dual Execution: The downloaded code was executed two ways simultaneously — first via eval() directly in the current Node.js process, and second by spawning a detached background node process that ran independently and survived even after the build finished.
  • Stage 4 — Automatic Trigger: Because postcss.config.mjs loads on every build, every time any developer ran npm run build or npm run dev, the entire chain fired again silently in the background.
postcss.config.mjs
│
├── export default config;   ← last legitimate line
│
└── [500+ spaces — pushes payload off screen]
    ├── global.i="A11--#";   ← malware marker
    ├── Query Ethereum RPC → find attacker wallet tx
    ├── Decode tx.to bytes → extract C2 server IP
    ├── HTTP GET to C2 server → download XOR payload
    ├── Decrypt payload with hardcoded key
    ├── eval(payload)   ← execute in current process
    └── spawn("node", ["-e", payload], {detached: true})   ← background process

The Ethereum approach is genuinely sophisticated. Traditional C2 malware hardcodes a domain or IP — which can be blocked, sinkholed, or used to track the attacker. Using a blockchain transaction as a dead drop for the server address means it is globally distributed, censorship-resistant, and effectively impossible to take down without the attacker's private key.

The Attacker Left Evidence: Windows Temp Files in .gitignore

For all the sophistication of the payload, the attacker made one sloppy mistake. Their injection script ran on a Windows machine and generated temporary batch files during execution. They tried to hide these by adding entries to .gitignore — but the .gitignore modification itself became evidence. In every infected repo I found, these three lines had been quietly added:

# Added by attacker to hide their Windows temp files
temp_auto_push.bat
temp_interactive_push.bat
branch_structure.json

These are Windows BAT files — the attacker was running an automated push script on a Windows machine. The script likely cloned each repo, injected the malware after the legitimate postcss export, committed with a plausible-sounding message, pushed, then deleted the local clone. The .gitignore entries were meant to prevent the temp files from showing up in git status — but they became a consistent fingerprint that I could search for across all repositories.

Timeline: 15 Months of Silent Access

Once I had the injection pattern, I could date every infection precisely by scanning commit history. The attack spanned 15 months — from April 2025 to July 2026. Eleven repositories were infected. The earliest infection was in April 2025. The most recent was just weeks before I discovered it — the attacker was still active and still injecting when I found it.

There was also a secondary attack pattern I found on one specific repository: an unauthorized automated script making daily docs-style commits at exactly the same time each day, for 38 consecutive days. The commits looked completely legitimate — updating documentation, tweaking configuration. But no GitHub Actions workflow existed in that repo to explain them. Someone with my token was running a cron job against my account.

How the Attacker Likely Got My Token

My best theory based on the evidence: my GitHub personal access token was stored in a configuration file or automation script at some point — likely when I was rapidly prototyping and hardcoded it rather than using proper environment variables. Once that config was accessible, someone extracted the token. After that, they had everything — full repo access to clone, inject, push, and delete, all authenticated as me.

The lesson here is uncomfortable in its simplicity: a leaked token is not just read access to your code. It is the ability to become you on GitHub — to push as you, to modify history as you, to plant code that runs on your build infrastructure as you. And it can operate silently for over a year before anything visible goes wrong.

The Cleanup: Removing Malware From Every Commit in History

Removing the malware from the current files was the easy part. But that is not enough. Every developer who clones the repo and checks out any historical commit would still get the malware from that snapshot. The infected commits exist in git history and are downloadable by anyone.

The proper fix is a full history rewrite using git filter-repo — a tool that can rewrite every commit across every branch, modifying file contents while preserving timestamps, authorship, and all other metadata. I wrote a Python blob callback that scanned every version of postcss.config.mjs across all commits and stripped everything after the legitimate export default config line.

# Install git filter-repo
pip install git-filter-repo

# Rewrite full history — strip malware from every commit
git filter-repo --blob-callback '
if b"postcss.config" in filename:
    content = blob.data.decode("utf-8", errors="replace")
    marker = "export default config;"
    idx = content.find(marker)
    if idx != -1:
        clean = content[:idx + len(marker)].rstrip() + "\n"
        blob.data = clean.encode("utf-8")
'

# Force push rewritten history
git push origin main --force

After the rewrite I verified every previously infected commit SHA using git cat-file — all returned not found, confirming the malware no longer existed anywhere in the repo history. Eleven repositories cleaned, history rewritten, old infected SHAs permanently gone.

What the Attacker Could Have Done With This Access

The eval plus spawn execution model means the attacker could download and run literally any code on any machine that ran a build on any infected project. In practice, this most likely meant reading environment variables — database connection strings, Supabase keys, API tokens, Vercel secrets — and exfiltrating them silently. Every build on every infected project over 15 months was a potential data exfiltration event.

After cleaning the repos, I rotated all credentials across every affected project. If there is any chance the attacker was intercepting environment variables during builds, everything stored there must be treated as compromised regardless of whether you have direct evidence of exfiltration.

Checklist: What You Should Do Right Now

  • Open every postcss.config.mjs and postcss.config.js in your projects. Use a text editor with line-length indicators or a hex viewer. Malware is often appended after the last legitimate line, hidden behind hundreds of spaces that look like whitespace at the end of the file.
  • Search your repos for temp_auto_push.bat, temp_interactive_push.bat, and branch_structure.json in .gitignore files. Finding any of these is a strong indicator of this specific attacker's fingerprint.
  • Rotate your GitHub personal access tokens immediately. Treat any token that was ever written to a file, config, or environment variable as potentially compromised.
  • Go to GitHub Settings and check both OAuth Apps and SSH Keys. Remove anything you do not recognize or no longer use.
  • If you find an infection, clean current files AND rewrite history with git filter-repo. Current-file cleanup alone leaves every historical commit still infected.
  • After cleanup, rotate every credential that existed in any environment variable accessible during your build process. This includes database passwords, Supabase service keys, external API tokens, and deployment secrets.

The Bigger Picture: Supply Chain Attacks Are Getting Smarter

What made this attack hard to catch was not the payload sophistication — it was the vector. postcss.config.mjs is a boring configuration file that every Next.js project has. Nobody reads it on code review. It is not in your application logic, not in your API routes, not anywhere a developer would think to look for malicious code. The attacker chose it deliberately because it loads automatically with full Node.js process permissions on every single build.

The Ethereum blockchain as an IP dead drop adds another layer of resilience. Traditional C2 infrastructure depends on domains or IPs that can be seized, blocked, or sinkholed. A blockchain-based delivery mechanism cannot be taken down — as long as the Ethereum network exists and the attacker's wallet has a transaction, the malware can find its server. This is a technique previously seen in high-sophistication threats and it is now appearing in token-theft attacks targeting individual developers.

The developer perimeter is not the npm registry anymore. It is not your CI environment or your Docker image. It is your personal access token — and if that token leaks, an attacker can plant code in your own repositories that looks like your work, runs as your builds, and operates silently for as long as you let it.

Final Note

I am sharing this because I found almost nothing written about this specific pattern — Ethereum-based C2 embedded in postcss config files, injected via stolen GitHub tokens — when I was in the middle of investigating it. If this post helps one developer catch a similar infection before it runs for 15 months undetected, it was worth writing in full detail. Check your config files. Rotate your tokens. Treat any build environment as potentially hostile until you have verified it. And if Vercel sends you a notification for a build you did not trigger — investigate immediately.

Key Insight

The attacker did not steal your code. They planted a factory inside it — one that silently phoned home on every single build, for 15 months, while you kept shipping.

Share this article