Maintainer account compromise: how attackers actually get in
Most documented npm and PyPI supply-chain attacks involve a compromised maintainer account. Understanding how those compromises actually happen — not in the abstract, but in the documented cases — is the foundation for hardening both as a maintainer and as a consumer of upstream packages.
This is a research note on the observed compromise vectors, with notes on what mitigates each one.
Vector: phishing with a credible page
The most common documented vector. A maintainer receives an email or DM that looks like it came from npm, PyPI, GitHub, or a related service. The message asks them to log in, re-verify, or claim a package — and links to a near-identical clone of the legitimate login page. The maintainer enters credentials; the attacker captures them.
What makes it work:
- Targeted maintainers of popular packages are public information. Email addresses are sometimes in package metadata, GitHub profiles, or commit history.
- Phishing pages are cheap to host and convincing to assemble.
- Maintainers receive legitimate emails from these services often enough that an additional one is not unusual.
Mitigations:
- Hardware-key-based 2FA (FIDO2/WebAuthn) is phishing-resistant. The browser binds the credential to the origin; a phishing site at a near-identical domain cannot complete the WebAuthn challenge. Both npm and PyPI support hardware-key 2FA. This is the single highest-leverage protection a maintainer can add.
- Password managers that auto-fill only on the matching origin. Manually typing a password into a phishing page is the failure mode; a password manager that refuses to auto-fill on a wrong domain is friction at exactly the right moment.
Vector: credential reuse
A maintainer's password leaks from another service (a forum data breach, an old SaaS provider). The same password works on the npm or PyPI account. The attacker logs in, possibly bypasses 2FA via account-recovery weaknesses, and publishes.
What makes it work:
- Password reuse is still common despite a decade of advocacy against it.
- Old accounts often pre-date 2FA enforcement; maintainers may never have enabled it.
Mitigations:
- Unique password per account. A password manager with generated passwords closes this entirely.
- Mandatory 2FA on the registry side means a leaked password isn't sufficient. Both npm and PyPI now enforce 2FA for the most-used accounts.
Vector: domain expiry on the registered email
Covered in detail in a separate post on ctx and phpass. The maintainer's email address is on a domain that lapses; an attacker registers the domain, receives password-reset links, and takes the account.
Mitigations:
- Use a personal email on a long-stable domain (Gmail, Outlook) for registry accounts. Tying registry access to a domain you may stop renewing is a known trap.
- Mandatory 2FA, again. The reset link alone shouldn't suffice; a second factor closes the loop. This requires registries to enforce 2FA on account recovery, which not all do consistently.
Vector: session token theft
Less common but observed. The maintainer's machine is compromised — through a malicious dependency in a different project, a browser extension, or similar — and the attacker exfiltrates session cookies or stored credentials. The attacker then uses the stolen session to publish.
What makes it work:
- Browser session cookies persist for days or weeks.
- Long-lived API tokens (
~/.npmrc) survive shell restarts and machine reboots. - A self-replicating malicious dependency is exactly the worm pattern documented in the
shai-huludpost — once one machine is compromised, every credential it can find is exposed.
Mitigations:
- Short-lived OIDC publishing (Trusted Publishing) removes the long-lived token entirely. A worm scanning for
~/.npmrcfinds nothing useful. - Granular tokens scoped per-package or per-task. A general publish token compromise affects every package the maintainer owns; a token scoped to one package limits the blast radius.
- 2FA on every publish action, not just on login. Both npm and PyPI support this; npm enforces it for top packages by default.
Vector: OAuth grant or app authorization
The maintainer authorizes a third-party app or service to access their account, with broader permissions than they realized. The app — or its provider — is compromised, and the attacker uses the granted access to publish.
What makes it work:
- OAuth scope grants are easy to over-approve. "Publish on my behalf" is sometimes the default ask.
- Apps that aggregate access across many maintainers become high-value targets.
Mitigations:
- Audit authorized apps quarterly. GitHub, npm, and PyPI all let you list active OAuth grants. Revoke any you don't recognize or no longer need.
- Use scoped tokens for service integrations rather than broad OAuth grants where possible.
Vector: insider threat or social engineering of the handoff
The event-stream story: a polite stranger offers to take over an unmaintained package; the legitimate maintainer transfers publish rights. There is no compromise — the attacker is granted access by the rightful owner.
Mitigations are limited:
- Don't transfer publish rights to people you cannot vet. This is hard advice for unmaintained packages whose owners genuinely want to walk away.
- Treat newly added co-maintainers as a code-review event. A new maintainer's first publish is a reasonable place to require additional scrutiny on the consumer side — extended cooling, peer review of the new version's diff.
What consumers can do
Most of the above are maintainer-side hardenings. From the consumer side, your defense is structural:
- Pin and hash via lockfiles. A compromised maintainer can publish a malicious version, but it doesn't enter your tree until you actively update.
- Cooling-period gates. A compromised account's first malicious publish hits the same gate as any other brand-new version.
- Provenance verification when available. A maintainer-account compromise without access to the registered build environment cannot produce a valid attestation.
Takeaway
Maintainer account compromise has a small number of recurring vectors. Phishing-resistant 2FA via hardware keys closes most of them. Trusted Publishing closes the long-lived-token class entirely. As a consumer, you can't prevent the compromise, but you can ensure that a compromised account's first malicious publish doesn't land in your dependency tree until someone has had a chance to look at it.