Safeguard
Concepts

What is Repo-Jacking

Repo-jacking hijacks renamed or deleted GitHub namespaces to serve attacker code at trusted URLs. Here's how the redirect trick works and how to audit your exposure.

Jonas Meyer
Site Reliability Engineer
7 min read

Repo-jacking is an attack in which someone re-registers an abandoned or renamed GitHub username or organization, recreates a repository at its old path, and thereby serves attacker-controlled code to everything that still fetches from the original URL. The attack exploits a convenience feature: when a GitHub account renames itself, GitHub redirects the old URLs to the new location — but if anyone claims the old name, the redirect breaks and the squatter's content wins. Every Go module path, GitHub Actions reference, curl | bash installer, Dockerfile, and README instruction pointing at the old path now resolves to whatever the attacker pushed. No phishing, no credential theft, no CVE. Just a username that became available.

The mechanics: rename, redirect, re-register

GitHub's redirect behavior is the whole story, so it's worth being precise:

  1. User oldname owns oldname/coolib, which other projects reference by URL.
  2. oldname renames to newname (or deletes the account). GitHub sets up a redirect: github.com/oldname/coolib transparently serves newname/coolib. Clones, go get, raw file fetches — everything keeps working, so downstream consumers never notice or update.
  3. Months later, an attacker registers the now-free username oldname and creates a repository named coolib.
  4. The redirect is severed. github.com/oldname/coolib now serves the attacker's repository, to every consumer that never updated the reference.

Step 2 is what makes this insidious: the redirect hides the breakage that would otherwise force consumers to fix their references. The dependency keeps resolving for years, right up until it resolves to something hostile.

GitHub knows about this and ships a partial defense called popular repository namespace retirement: a namespace with more than 100 clones in the week before its owner renamed or deleted can't be reused with the same repo name. Two problems. The 100-clone threshold leaves the long tail — which is most of the open source graph — unprotected. And security researchers, notably Checkmarx, have repeatedly found bypasses for the retirement mechanism, several of which GitHub has had to patch. Treat the protection as a speed bump, not a guarantee.

How big the exposure actually is

In June 2023, Aqua Security's team sampled 1.25 million repositories and found roughly 2.95 percent vulnerable to repo-jacking — references to GitHub namespaces that had been renamed or deleted and were available for registration. Extrapolated across GitHub, that's millions of repositories carrying at least one hijackable reference, including projects belonging to Google and Lyft that were quietly fixed after disclosure.

The exposure concentrates in places where a repository URL is the package identity:

  • Go modules. import "github.com/oldname/coolib" — the module path is the fetch URL. Go's checksum database (sum.golang.org) and module proxy protect existing consumers: a hijacked module with different contents fails go.sum verification. But new consumers, and anyone fetching a version the proxy never cached with GONOSUMDB/GOPRIVATE misconfigured, get the attacker's code cleanly.
  • GitHub Actions. uses: oldname/some-action@v1 resolves by namespace, and tags are mutable. A hijacked action executes in your CI with your secrets in scope. Pinning by full commit SHA (uses: oldname/some-action@a1b2c3d...) defeats this, because the attacker can't forge a commit hash.
  • Installer one-liners. curl -sSL https://raw.githubusercontent.com/oldname/tool/main/install.sh | bash in READMEs, Dockerfiles, and bootstrap scripts. These have no integrity check at all; whoever controls the namespace controls root on your machine.
  • Package manifests pointing at Git. npm's "dep": "github:oldname/coolib", pip install git+https://github.com/..., Ruby gems with git: sources. Lockfiles that record a commit hash help; ranges and branch references don't.

Auditing your own exposure

You can get a useful answer in an afternoon. First, inventory every GitHub reference your builds consume:

# GitHub Actions references (excluding local and SHA-pinned)
grep -rhoE "uses:\s*\S+" .github/workflows/ | sort -u

# Go module requirements
grep -hE "^\s+github.com/" go.mod

# Git-sourced deps in npm/pip/gem manifests
grep -rE "github:|git\+https://github" package.json requirements*.txt Gemfile

# Raw fetches in Dockerfiles and scripts
grep -rE "raw.githubusercontent.com|github.com/.+/(archive|releases)" --include=Dockerfile --include="*.sh" .

Then, for each unique owner/repo, check whether the owner still exists and whether the URL redirects: curl -sI https://github.com/OWNER returning 404 for a namespace your builds fetch from is a fire alarm — anyone can register it right now. A 301 on the repo URL means you're currently riding a rename redirect and should update the reference to the new location today, before the old name frees up.

This is dependency inventory work, which is to say it's exactly what a software composition analysis pipeline should be doing continuously rather than an engineer doing annually — Git-sourced dependencies and unpinned action references are inventory entries like any other, and Safeguard flags redirect-riding references as part of repository scanning.

Defenses that actually hold

DefenseProtects againstCost
Pin actions and Git deps by commit SHAHijacked namespace serving new contentLow — Dependabot/Renovate update pins
Update references after upstream renamesRedirect severanceLow, but requires noticing the rename
Vendor or mirror critical dependenciesUpstream disappearing entirelyMedium — storage and sync process
Go module proxy + checksum DB (default on)Content substitution for known versionsFree — don't disable GOSUMDB
Replace curl-pipe-bash installers with checksummed releasesInstaller hijackLow — verify sha256sum against a pinned value
Internal artifact proxy for all fetchesDirect-from-GitHub surprises in CIMedium — but you want this anyway

The organizational defense matters as much as the technical ones: when you rename an org or username, keep the old name registered — GitHub lets you claim it back immediately with a placeholder account — and hold it forever. It costs nothing and permanently forecloses the attack against your downstream users. The number of companies that rename acme-inc to acme and let the old name float away is remarkable, and their open source consumers inherit the risk.

For the broader family of namespace attacks — this is a sibling of dependency confusion and typosquatting, all exploiting resolution-by-name — the Academy has a full module on package and namespace integrity.

Frequently asked questions

Is repo-jacking the same as dependency confusion?

They're cousins. Dependency confusion exploits a resolver preferring a public registry package over your internal one; repo-jacking exploits a namespace becoming re-registrable so an existing trusted URL changes hands. Both deliver attacker code through a name you already trust, which is why SHA pinning and artifact proxies mitigate both.

Does GitHub's namespace retirement make this a solved problem?

No. Retirement only covers namespaces with roughly 100+ clones in the week before the rename, the long tail is unprotected, and researchers have found repeated bypasses of the mechanism itself. It reduces opportunistic hijacks of famous projects; it does not protect the average transitive reference.

Are lockfiles enough protection?

Lockfiles that record a commit hash or content checksum protect the versions you've already resolved — a hijacked repo can't match the recorded hash. They don't protect first-time installs, version bumps, or anything resolved from a branch or tag reference, so pin by SHA at the source too.

What should I do if a namespace I depend on returns 404?

Treat it as an active incident, not a chore. Vendor the code you're currently using from your local cache or lockfile-verified copy, remove the live reference from builds, and only then investigate whether the project moved somewhere legitimate. If the namespace is available, someone hostile can claim it in the time it takes you to file a ticket.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.