Security concepts

What is package hijacking?

Hijacking is an ownership failure, not a name collision. Attackers take maintainer accounts, expired reset-email domains, abandoned repo URLs, Go import paths after a GitHub rename (Chainjacking, disclosed 16 November 2021), or a browser CDN hostname (Polyfill.io, acquired February 2024, malicious injections reported June 2024). Hold domains, require release PRs (Release Please), and pin what you serve.

9 min read
In short: Hijacking is an ownership failure, not a name collision. Attackers take maintainer accounts, expired reset-email domains, abandoned repo URLs, Go import paths after a GitHub rename (Chainjacking, disclosed 16 November 2021), or a browser CDN hostname (Polyfill.io, acquired February 2024, malicious injections reported June 2024). Hold domains, require release PRs (Release Please), and pin what you serve.

What is package hijacking?

Package hijacking is a supply-chain attack in which someone else gains the ability to publish new versions of a package you already trust, or to serve a replacement from a URL you already pin. The installer is not confused about the name. The name is correct. The publisher is not.

A dependency is any external module or library your project needs. A package is how that library is distributed: code, metadata, and docs, installed by npm, pip, or go get. Hijacking is different from dependency confusion, where a public name collides with a private one. It is also different from depending on a trivial package that you could have written yourself, although a neglected trivial package is a frequent hijack target.

Once a hijacked version is on the registry, every project that floats that range installs it. The malicious build usually still "works," which is why the compromise can sit in production until someone reads the diff.

How do attackers hijack a package?

VectorWhat the attacker takesTypical tell
Compromised maintainer accountCredentials, a leaked token, or a phished npm/PyPI loginUnexpected publish, new maintainer, disabled 2FA
Expired domain / email takeoverThe domain that receives password-reset mail for the maintainerWHOIS shows the domain available or re-registered
Repo jackingAn abandoned GitHub (or similar) URL that installers still fetch404 on the old owner, then a new owner with the same path
Chainjacking (Nov 2021)A retired GitHub username plus a Go import path

go.mod still points at github.com/olduser/lib

CDN / domain acquisition (Polyfill.io, 2024)

The hostname millions of sites <script> tag

Ownership change on a browser-side URL, not a lockfile

Public registries are community-writable. Private registries are not safer if the token that publishes to them lives in a CI variable that fifty workflows can read. In both cases the question is the same: who can create the next version, and how would you notice?

How do compromised accounts take over a package?

Stolen passwords and leaked automation tokens are the common path. A breach at some other site is enough if the maintainer reused the password. Phishing that impersonates npm or GitHub is enough if the account has no hardware key. A registry with no rate limit on login is enough for stuffing.

XSS or injection against the registry itself is rarer and higher impact. Weak collaborator lists are ordinary: a former contractor who can still npm publish. Have I Been Pwned (and the same check inside 1Password) is the monitoring baseline. Turn on 2FA with a TOTP app or a security key, not SMS. Prefer short-lived granular tokens or trusted publishing over a classic npm token in NPM_TOKEN. Google's phishing research is worth taking literally: drills that only teach "do not click" fail; incident reporting that is fast matters more.

How does an expired domain become a package takeover?

If the maintainer's account email is admin@oldproject.net and oldproject.net lapses, the attacker registers the domain, recreates the mailbox, and uses "forgot password." They now publish as the maintainer. The package page still shows the same name and the same avatar until someone reads the whois.

Turn on auto-renew. Pipe expiry and transfer notices into a ticket queue, not a personal inbox that goes quiet when someone leaves. Monitor the domains you use for npm, PyPI, and GitHub account mail. If a domain is already gone, remove that email from every registry and rotate tokens before you negotiate a buy-back you may not win.

What happened with Polyfill.io in 2024?

Polyfill.io was a CDN that generated JavaScript shims so older browsers could run modern APIs. Sites included it with a script tag, not a lockfile:

<script src="https://cdn.polyfill.io/v3/polyfill.min.js"></script>

In February 2024 the domain and the associated GitHub account were acquired by Funnull, a CDN operator. Andrew Betts, the original author, had already warned that he did not control the domain and that modern browsers no longer needed the service. In June 2024 Sansec and others showed that cdn.polyfill.io was injecting malicious JavaScript into more than 100,000 embedding sites. One decoded payload redirected mobile users to betting and scam pages via a typosquat (www.googie-anaiytics.com). The code avoided admin sessions, delayed when analytics scripts were present, and varied by request headers.

Google began warning advertisers and blocking infected landing pages. Namecheap suspended the domain after reports on 27 June 2024. Fastly and Cloudflare stood up clean drop-in hostnames for teams that still needed a polyfill. The lasting lesson is not "CDNs are bad." It is that a hostname in a template is a dependency with no package-lock.json, no checksum in CI, and no 2FA on your side. An acquisition of that hostname is a publish. Pin a copy you serve, or delete the tag. Internet Explorer is not a reason to keep it.

What is repo jacking?

Repo jacking targets version-control URLs instead of registry names. If github.com/alice/useful is in a Makefile or a go get line and Alice deletes or renames the account, a new Alice can create useful and serve different source at the same path. Anyone still fetching the old URL is compromised. The package manager never saw a new name, so confusion defenses do not apply.

Review repository references. Do not rename accounts unless you must. Sign commits and enable GitHub vigilant mode so unsigned commits show as unverified. Checksums confirm integrity of a blob you already trust; they do not authenticate a new owner of the URL.

What is chainjacking, and when was it disclosed?

On 16 November 2021, Alik Koldobsky (Checkmarx) and Dr. Joakim Kennedy (Intezer) disclosed Chainjacking. Go has no central module registry. go get fetches github.com/user/repo. GitHub redirects URLs after a rename. When the old username becomes available, an attacker who registers it and creates a repo of the same name breaks the redirect. Imports that still say github.com/olduser/repo now resolve to the attacker.

GitHub's "popular repository namespace retirement" was the existing mitigation: a path with more than 100 clones in the week before the rename or deletion is retired and cannot be reused. The researchers showed that retirement could still be bypassed, at the time, through another rename bug, a repository transfer, a deleted-repo restore, and a race. They reported to GitHub on 8 October 2021 and to the Go security team on 5 November 2021. Go's reply (10 November) treated it as a hosting problem, not a toolchain fix. Pin a module checksum in go.sum (the default) and prefer a vanity import path you control so a GitHub username change is not your public API.

How do you reduce package-hijacking risk?

Automate what you can. Socket (which Arcjet uses) and Dependabot catch a lot of the review. The rest is ownership.

Hold the domain. Auto-renew, monitor expiry, and keep the admin mailbox staffed.

Terminal window
# Fail CI if a maintainer email domain is unregistered or expired.
# JFrog's npm_domain_check is one implementation of this check.
npx npm_domain_check

Control releases. Require a pull request, a changelog, and a second approver before publish. For the Arcjet JavaScript SDK that workflow is Release Please: the bot opens a release PR from conventional commits, and merging it tags the repo and publishes. The audit log is the pull request, not a laptop that ran npm publish.

{
"release-type": "node",
"include-v-in-tag": true,
"changelog-types": [
{ "type": "feat", "section": "Features", "hidden": false },
{ "type": "fix", "section": "Bug Fixes", "hidden": false }
]
}
.github/workflows/release-please.yml
on:
push:
branches: [main]
permissions:
contents: write
pull-requests: write
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@v4
with:
release-type: node

Prefer npm trusted publishing (OIDC) so CI does not store a write token at all.

Stay current on advisories. Dependabot or an equivalent is how this happens in practice. You will not subscribe to every mailing list by hand.

Review the diff, not the readme. Obfuscated or unexpected native addons in a patch release are the usual payload. Minified code is not automatically hostile, but it is not reviewable either.

Hijacking is an ownership failure that looks like a routine upgrade. Know who can publish, know which domains and GitHub names that trust depends on, and make the next version go through a PR you can still read after the author has left.

Frequently asked questions

What is package hijacking?

It is a supply-chain attack where someone else gains the ability to publish new versions of a package you already trust, or to serve a replacement from a URL you already pin. The name is correct; the publisher is not.

How is hijacking different from dependency confusion?

Confusion installs a public package that shares an internal name. Hijacking takes over the real name or the real URL.

When was Chainjacking disclosed?

16 November 2021, by Alik Koldobsky (Checkmarx) and Dr. Joakim Kennedy (Intezer). Go fetches GitHub URLs; a reused username plus a matching repo name breaks GitHub's rename redirect.

What was the Polyfill.io incident?

In February 2024 Funnull acquired polyfill.io. In June 2024 researchers showed cdn.polyfill.io was injecting malicious JavaScript into more than 100,000 sites. A hostname in a script tag is a dependency with no lockfile.

Application security in your code

Protect your application with Arcjet

Get rate limits, bot detection, and attack blocking in your request handlers.