Back to glossary

Maintainer Account Takeover

Maintainer account takeover is when an attacker gains a package maintainer's registry access and publishes malicious versions under the trusted name.

Also known as ATO, publisher account compromise, npm maintainer account takeover

Maintainer account takeover (ATO, also called publisher account compromise) is a supply chain attack in which an attacker seizes a maintainer's registry identity, via a phished password, hijacked session, stolen token, or recovered email, and publishes malicious versions under the legitimate package name. The registry sees an authorized publisher, so every version range and automated update trusts it.

The root cause is a gap in what a registry checks. When npm publish runs, the registry answers one question: is this credential allowed to publish this name? It never asks whether the code is what the maintainer intended. That gap is behind several of the most damaging npm incidents of the past 14 months. The Qix compromise on September 8, 2025 hit packages that together see 2 to 3 billion downloads a week. The axios compromise on March 31, 2026 pushed a remote access trojan through a package with 100 million weekly downloads.

Our position: consumers should assume any maintainer can be taken over, and treat every new version from a trusted name as unreviewed code until its contents have been analyzed. Producer-side controls are improving quickly. npm's staged publishing is the first one that puts a human 2FA check between a stolen credential and the public registry. None of them stops an attacker who already controls the maintainer's machine.

What is account takeover, and why are maintainer accounts different?

#

Account takeover (ATO) is unauthorized control of an existing, legitimate account. The usual routes are the same everywhere: passwords reused from breached sites, phishing pages that relay credentials and one-time codes, session cookies lifted by infostealer malware, and abuse of password reset flows. For a bank or email account, the damage is bounded by what that one account holds.

A maintainer account is different because its value is a write primitive on other people's machines. One publish reaches every project whose semver range admits the new version, the next time that range is resolved fresh. Nobody reviews it on the way.

To see why the registry cannot tell the difference, look at what a publish actually is. npm publish packs the tarball, computes its integrity hash on the client, and sends a single PUT to the registry. This is the request shape that libnpmpublish builds in npm 10.9.9:

HTTP
PUT /example-pkg HTTP/1.1
Host: registry.npmjs.org
Authorization: Bearer npm_<token or session>
npm-otp: 492817
Content-Type: application/json

{
  "_id": "example-pkg",
  "name": "example-pkg",
  "dist-tags": { "latest": "1.4.1" },
  "versions": {
    "1.4.1": {
      "name": "example-pkg",
      "version": "1.4.1",
      "scripts": { "preinstall": "node setup.mjs" },
      "dist": { "integrity": "sha512-<computed by the client>" }
    }
  },
  "_attachments": {
    "example-pkg-1.4.1.tgz": { "data": "<base64 tarball>" }
  }
}

The registry validates the credential, confirms the user is a maintainer of example-pkg, checks the 2FA requirement (an npm-otp header, a token configured to bypass 2FA, or an OIDC token from a trusted publisher), confirms the version number is unused, and moves latest. Every check is about identity. None inspects the tarball.

The integrity hash does not help either. The publisher computes it, so a malicious publisher supplies a correct hash for malicious code. The worm in the keyv and cacheable compromise did exactly that: it injected its hook, recomputed integrity and shasum, bumped the version, and issued the PUT itself. Lockfile hashes prove you got the bytes the publisher uploaded. They say nothing about who the publisher was.

1 · How the attacker gets in

  • Reused passwordeslint-scope, 2018
  • Phishing page captures a login, 2FA code, or tokeneslint-config-prettier and Qix, 2025
  • Malware on the maintainer's machineaxios, 2026
  • Token stolen from a laptop or CI runnerShai-Hulud v2, Mini Shai-Hulud
  • Password reset via an expired email domainnode-ipc, 2026

2 · What the registry receives

  • A web or CLI session plus a 2FA code or key signature
  • A granular access token, possibly set to bypass 2FA
  • An OIDC token from a CI workflow bound as a trusted publisher

3 · What npm checks on PUT /<name>

  • Credential is valid
  • User is a maintainer of the name
  • 2FA policy is satisfied
  • Version number is unused
Never checkedTarball contents, whether the code matches the repository, whether the maintainer meant to publish it
Figure 1. Every takeover route ends in a credential the registry accepts. The registry's four checks are all about identity, so a malicious version from a taken-over account passes each one.

Figure 1 traces the path end to end. Five entry routes (reused passwords, phishing pages that capture logins, 2FA codes, or tokens, malware on the maintainer's machine, tokens stolen from laptops or CI runners, and password resets through expired email domains) each yield one of three credentials: a session with a 2FA code or key signature, a granular access token, or an OIDC token from a CI workflow bound as a trusted publisher. The registry then checks four things, all about identity: the credential is valid, the user maintains the name, the 2FA policy is met, and the version is unused. It never checks the tarball's contents, whether the code matches the repository, or whether the maintainer meant to publish it.

How do attackers take over npm maintainer accounts?

#

Attackers take over npm maintainer accounts through five routes: reused passwords, phishing, compromise of the maintainer's workstation, theft of publish tokens, and account recovery through an expired email domain. Each route ends with the same thing, a credential the registry accepts.

Reused passwords

#

The template was set on July 12, 2018. An ESLint maintainer, in the project's words, "had reused their npm password on several other sites and did not have two-factor authentication enabled." The attacker published eslint-scope@3.7.2, which sent each installer's .npmrc, and the publish tokens inside it, to the attacker. npm responded by revoking every access token issued before 12:30 UTC that day. The payload's goal is the important part: a takeover is most valuable when it harvests the credentials for the next takeover.

Phishing

#

Registry phishing now targets the 2FA flow itself. The eslint-config-prettier compromise in July 2025 began with an email from the lookalike domain npnjs.com that captured a maintainer's npm token. In the Qix compromise, the lure was a "2FA reset" email from support@npmjs.help. The same day, debug@4.4.2, chalk@5.6.1, and more than a dozen other versions of Qix's packages carried a crypto-wallet drainer. Attackers do not have to guess targets: every packument lists each maintainer's account email.

Bash
$ curl -s https://registry.npmjs.org/axios \
    | jq -r '.maintainers[] | "\(.name)\t\(.email | split("@")[1])"'
jasonsaayman	gmail.com

We print only the domain here, but the response contains the full address. A time-based one-time password (TOTP) code is phishable because a fake page can relay it to npm in real time. A WebAuthn security key is not, because the browser binds each signature to the origin that requested it, and npnjs.com is not npmjs.com. npm stopped accepting new TOTP setups on October 13, 2025; existing TOTP enrollments still work.

Workstation compromise

#

The axios takeover did not start with a credential. A roughly two-week social engineering campaign used a cloned company identity, a staged Slack workspace, and staged meetings to get the lead maintainer to install remote access malware. The maintainer later described the result: the attacker had "enough access to hijack from my browser… lifting sessions or cookies." With a live session on the maintainer's own machine, 2FA and OIDC publishing were irrelevant. The same playbook targeted maintainers of Lodash, Fastify, dotenv, and Node.js core in the same weeks, and Mandiant linked the campaign to a North Korea-nexus actor.

Stolen tokens and worms

#

The highest-volume takeovers are now automated. Credential-stealing malware runs at install time on a developer laptop or CI runner, collects npm and GitHub tokens, and uses them to republish every package the victim can reach. The loop is short: call /-/whoami, list the packages that identity can publish, download each tarball, inject a preinstall hook, bump the patch version, and PUT it back.

Shai-Hulud v2 in November 2025 showed how a CI foothold becomes a registry credential. At PostHog, a pull request abused a pull_request_target workflow to steal a bot's GitHub token, then the rest of the repository secrets, including an npm publish token. On May 19, 2026, Mini Shai-Hulud used the hijacked atool account to publish 639 malicious versions across 323 packages in the @antv ecosystem in about an hour. npm's response the same day was to invalidate every write-capable granular token that bypassed 2FA. Maintainers issued new ones, and the worm went back to harvesting them.

Expired email domains

#

The quietest route skips the maintainer entirely. If a maintainer's account email sits on a domain that lapses, whoever registers the domain can receive the password reset. On May 14, 2026, the node-ipc compromise followed that pattern: a maintainer account that had been dormant on the package for years published node-ipc@12.0.1, 9.2.3, and 9.1.6 within 55 seconds, and the likely vector, first identified by an independent researcher, was a password reset sent to the account's recovery email on an expired domain. The previous 9.1.x release, 9.1.5, was published on July 1, 2021. PyPI saw the same attack in 2022 and since June 2025 has unverified more than 1,800 email addresses whose domains entered expiry.

Producer controls mapped to takeover routes

#

Phishing-resistant multi-factor authentication, trusted publishing, and staged publishing are the three producer-side controls npm now offers. They are often presented as interchangeable upgrades. They defend different routes.

Takeover routeReal exampleSecurity-key 2FATrusted publishing (OIDC)Staged publishing
Reused passwordeslint-scope, 2018Stops itNo effectNo effect on its own: a human with a valid login can still publish directly
Phished login and one-time 2FA codeQix, 2025Stops it: key signatures are bound to the real originNo effectNo effect: a valid 2FA code still allows a direct publish
Stolen 2FA-bypass tokenShai-Hulud v2, 2025; @antv, 2026No effect: the token skips 2FAStops it only if no publish tokens remainStops it with stage-only tokens now, and for all bypass tokens from January 2027
Hijacked CI workflow or OIDC bindingTanStack and keyv, 2026No effectNo effect: the attack runs through itStops it if the trusted publisher may only stage
Malware on the maintainer's machineaxios, 2026Partial: the attacker must get the maintainer to touch the keyNo effectNo effect: the attacker can publish or approve as the maintainer
Password reset via expired email domainnode-ipc, 2026Stops it only if a key was enrolled before the domain lapsedNo effectNo effect: the attacker can enroll their own 2FA

Staged publishing, in public preview since May 20, 2026 with npm CLI 11.15.0, changes the shape of the problem. A workflow runs npm stage publish instead of npm publish, and the version becomes installable only after a maintainer runs npm stage approve <stage-id> or clicks Approve on npmjs.com. Per npm's documentation, "You will be prompted for 2FA verification whether you approve the package in the CLI or on npmjs.com." OIDC tokens cannot approve. Since September 18, 2026, maintainers can also mint stage-only tokens that can stage but not publish.

Our verdict: the three controls are complements, not substitutes. Security keys close the human routes that end in a login, reused passwords and relay phishing. Staged publishing closes the automated routes, stolen tokens and hijacked CI, which are the routes worms use. Trusted publishing on its own removes long-lived tokens but adds no human check. Nothing in the table fully covers a compromised workstation, where the attacker rides a live session and can wait for the maintainer to touch the key, or a dormant account that never enrolled 2FA. Those gaps are why consumers cannot outsource this problem to maintainers.

A takeover in registry metadata

#

A takeover looks like a legitimate release from a legitimate account, with one detail out of place. The detail is usually the publish path, not the publisher. Here is the axios history, pulled from the registry on October 27, 2026:

Bash
$ curl -s https://registry.npmjs.org/axios | jq -r '. as $p
    | ["1.13.3","1.13.4","1.13.5","1.13.6","1.14.0","1.14.1","1.15.0"][] as $v
    | "\($v)\t\($p.time[$v])\t\($p.versions[$v]._npmUser.name // "(removed)")\t\(if $p.versions[$v].dist.attestations then "provenance" else "-" end)"'
1.13.3	2026-01-25T14:21:38.842Z	jasonsaayman	-
1.13.4	2026-01-27T18:18:38.613Z	GitHub Actions	provenance
1.13.5	2026-02-08T11:05:13.696Z	GitHub Actions	provenance
1.13.6	2026-02-27T15:35:51.983Z	GitHub Actions	provenance
1.14.0	2026-03-27T19:01:40.915Z	GitHub Actions	provenance
1.14.1	2026-03-31T00:21:58.168Z	(removed)	-
1.15.0	2026-04-08T16:09:38.623Z	GitHub Actions	provenance

From 1.13.4 onward, every 1.x release came through the GitHub Actions trusted publisher with a provenance attestation. Then 1.14.1 appeared at 00:21:58 UTC with no matching Git tag, published from the maintainer's own account. axios@0.30.4 followed on the legacy branch 39 minutes later. Both were removed around 03:15 UTC, according to the project's post-mortem, but the registry's time object still records when they were published. That makes time a useful forensic record after a takeover.

1.x 0.x Feb 1, 2026 Mar 1, 2026 Apr 1, 2026 four releases via trusted publisher, with provenance +39 min 1.13.3Jan 25 1.13.4Jan 27 1.13.5Feb 8 1.13.6Feb 27 1.14.0Mar 27 1.14.1Mar 31 00:21 1.15.0Apr 8 0.30.3Feb 18 0.30.4Mar 31 01:00 1.13.3 · Jan 25 · 1.x line maintainer account, no provenance 1.13.4 · Jan 27 · 1.x line trusted publisher, provenance 1.13.5 · Feb 8 · 1.x line trusted publisher, provenance 0.30.3 · Feb 18 · 0.x line maintainer account, no provenance 1.13.6 · Feb 27 · 1.x line trusted publisher, provenance 1.14.0 · Mar 27 · 1.x line trusted publisher, provenance 1.14.1 · Mar 31 00:21 · 1.x line malicious, removed 0.30.4 · Mar 31 01:00 · 0.x line malicious, removed 1.15.0 · Apr 8 · 1.x line trusted publisher, provenance
  • GitHub Actions trusted publisher, provenance
  • Maintainer's own account, no provenance
  • Malicious, removed from the registry
Figure 2. axios releases by publish path, January 20 to April 12, 2026, from registry.npmjs.org. The malicious versions broke a run of trusted-publisher releases on 1.x. The legitimate 0.30.3 shows that the 0.x line was still published by hand.

Figure 2 adds the 0.x line to the same data. 0.30.3 was published from the maintainer's own account on February 18, 2026, with no provenance, three weeks after 1.x moved to trusted publishing. The malicious 1.14.1 (March 31, 00:21 UTC) and 0.30.4 (01:00 UTC) both came outside the pipeline. 1.15.0 on April 8 returned to the trusted publisher.

pnpm turns this signal into a policy. With trustPolicy: no-downgrade, pnpm refuses any version whose trust evidence is weaker than a version published earlier, which is the axios pattern. The same policy also shows the cost. The maintainer kept publishing the 0.x line by hand after the 1.x line moved to trusted publishing, so a legitimate release trips it:

Bash
$ pnpm add axios@0.30.3
[ERR_PNPM_TRUST_DOWNGRADE] High-risk trust downgrade for "axios@0.30.3" (possible package takeover)

Trust checks are based solely on publish date, not semver. A package cannot be installed if any earlier-published version had stronger trust evidence. Earlier versions had trusted publisher, but this version has no trust evidence. A trust downgrade may indicate a supply chain incident.

axios@0.30.3 is the version the axios post-mortem told 0.x users to roll back to. The policy is still worth turning on. Expect to maintain trustPolicyExclude entries for packages whose maintainers publish different branches through different paths.

Identity signals are weak against takeover for a simple reason: the identity is real. Socket's New author alert fires when an npm collaborator publishes a version for the first time, and Unstable ownership fires when a new collaborator begins publishing. Both catch an attacker who adds an account, and neither fires when the attacker publishes as the existing maintainer. The Non-existent author alert flags versions whose publishing account has since been deleted. What catches a takeover is the content: Socket flagged the payload dependency in the axios compromise within six minutes, detected the @antv wave with a median of 6.7 minutes after each publish, and flagged keyv artifacts an average of 5 minutes 18 seconds after publication.

Why don't provenance and trusted publishing stop account takeover?

#

Provenance and trusted publishing prove which pipeline built a package, not whether the maintainer intended the release. When an attacker controls the source repository or the CI workflow, the pipeline faithfully builds and attests the attacker's code.

keyv@6.0.0 is the clean example. The version shipped with a passing package provenance attestation, because the legitimate release workflow built source that the attacker, holding the maintainer account, had already trojanized. Its predecessor, 6.0.0-rc.1, published the day before, also came through the trusted publisher with provenance, so a trust-downgrade check sees no change. In the TanStack compromise in May 2026, the attackers pulled an OIDC token from a GitHub Actions runner's memory and published through the project's real trusted-publisher binding.

Provenance and signatures are necessary but not sufficient. Trusted publishing removes long-lived tokens, which shrinks the target, and provenance makes an out-of-pipeline publish like axios 1.14.1 visible. Neither detects an attacker who publishes through the pipeline.

How do you prevent account takeover?

#

You prevent account takeover by removing every credential that can publish without a fresh, phishing-resistant human check, and you contain it by assuming that removal will sometimes fail. The first half is the maintainer's job. The second is everyone else's.

For maintainers, as of October 2026:

  • Use a security key for npm 2FA. It defeats relay phishing. It does not stop an attacker on your machine who waits for you to touch the key.
  • Route every automated publish through staged publishing. Set the trusted publisher's allowed action to stage only, or use stage-only tokens. It costs an approval click per release, and it does not help if the approver is the person being socially engineered.
  • Delete 2FA-bypass tokens now. As of GitHub's July 31, 2026 changelog, they can no longer change account, maintainer, or trusted-publishing settings without 2FA. npm is targeting January 2027 to remove their direct publish rights. Until then, a stolen one still publishes.
  • Prune maintainers and own your recovery domain. Every dormant account with publish rights is an attack surface, as node-ipc showed. Logging in with a recovery code triggers a 72-hour publishing hold on npm, which helps. A domain that lapses does not trigger anything.

For consumers, the controls are about delay, execution, and content:

YAML
minimumReleaseAge: 1440      # default in pnpm 11: skip versions under 24h old
trustPolicy: no-downgrade    # refuse a version with weaker publish evidence
blockExoticSubdeps: true     # default in pnpm 11: no git or tarball subdeps
  • A dependency cooldown beats short exposure windows. The axios versions were live for about three hours, so a 24-hour minimum release age would have skipped them. It does nothing for a malicious version that stays undetected past the window, and a setting in one project's config does not cover npx runs or global installs outside it.
  • Turning off install scripts removes the default execution path. npm v12 now skips dependency lifecycle scripts unless you approve them, as pnpm 10 already did. The axios, @antv, and keyv payloads all ran from install hooks. The node-ipc payload did not: it ran when the module was loaded with require, so script blocking alone would have let it through.
  • Lockfiles protect existing installs, not new resolutions. npm ci against a committed lockfile would not have pulled axios@1.14.1. Adding a dependency, running a tool through npx, or merging an automated update PR resolves fresh against the registry, as our analysis of the axios blast radius showed. A clean lockfile checked afterward does not prove you were never exposed.
  • Analyze what is in the package before it installs. Socket Firewall checks each package request against Socket's verdicts and blocks known malware before the download completes. The free tier blocks only human-confirmed malware and lets unknown versions through, which is why it pairs well with a cooldown that holds new versions until a verdict exists.

None of these depend on a CVE. A takeover produces no vulnerability in the package, only malware published under its name, so CVE scanners have nothing to match.

The open problem: the registry still trusts the session

#

npm has spent 2025 and 2026 removing credentials that can publish on their own. Classic tokens were revoked on December 9, 2025. Bypass tokens are slated to lose direct publishing in January 2027. Staged publishing gives every package a place for a human to say no. These are real gains. npm creator Isaac Schlueter has called for converting every non-MFA publish into a staged publish by default, and we agree: staged publishing should become the default for any package above a meaningful download threshold, not an opt-in that maintainers wire up one package at a time.

The axios compromise points at what is left. The attacker's real asset was not a token but the maintainer's machine, and on that machine the human approval step is something an attacker can trigger, then wait for. The next generation of takeovers will aim at the approval itself, which is why the last line of defense has to sit with the people installing the code, and has to look at the code.