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:
- User
oldnameownsoldname/coolib, which other projects reference by URL. oldnamerenames tonewname(or deletes the account). GitHub sets up a redirect:github.com/oldname/coolibtransparently servesnewname/coolib. Clones,go get, raw file fetches — everything keeps working, so downstream consumers never notice or update.- Months later, an attacker registers the now-free username
oldnameand creates a repository namedcoolib. - The redirect is severed.
github.com/oldname/coolibnow 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 failsgo.sumverification. But new consumers, and anyone fetching a version the proxy never cached withGONOSUMDB/GOPRIVATEmisconfigured, get the attacker's code cleanly. - GitHub Actions.
uses: oldname/some-action@v1resolves 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 | bashin 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 withgit: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
| Defense | Protects against | Cost |
|---|---|---|
| Pin actions and Git deps by commit SHA | Hijacked namespace serving new content | Low — Dependabot/Renovate update pins |
| Update references after upstream renames | Redirect severance | Low, but requires noticing the rename |
| Vendor or mirror critical dependencies | Upstream disappearing entirely | Medium — storage and sync process |
| Go module proxy + checksum DB (default on) | Content substitution for known versions | Free — don't disable GOSUMDB |
| Replace curl-pipe-bash installers with checksummed releases | Installer hijack | Low — verify sha256sum against a pinned value |
| Internal artifact proxy for all fetches | Direct-from-GitHub surprises in CI | Medium — 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.