DEV Community

Noble Ronin
Noble Ronin

Posted on

I Found 53 Real Email Addresses Behind One Tiny npm Package

I Found 53 Real Email Addresses Behind One Tiny npm Package

Every npm supply-chain story this year has the same first sentence: somebody phished a maintainer. 144 packages in the Mastra ecosystem, this June. The Qix incident last September — 18 to 20 packages, a combined couple billion weekly downloads, all because one maintainer typed his 2FA code into a fake npm support page. eslint-config-prettier's maintainers got the same treatment in July 2025, via a different lookalike domain. Read enough of these writeups and you start wondering about the boring mechanical step nobody dwells on: how does an attacker even know who to email?

For two of the three major package registries, it turns out, the answer is: you don't have to guess. The registry just tells you.

53 email addresses, for a package that parses "2 days"

I started with ms, the little utility that turns strings like "2 days" into milliseconds. It's a dependency of a dependency of half of Node's ecosystem. One request to registry.npmjs.org/ms/latest and the maintainers array comes back with 53 entries, each with a name and a real, working-looking email address. Dozens are personal Gmail and iCloud addresses. A few are corporate — I count at least three @vercel.com addresses and two @google.com addresses among the maintainers list. No login required, no rate limit I hit, just a plain GET.

That's not a one-off. webpack has 8 maintainer emails, mixing Gmail, QQ.com, and a googlemail.com address. typescript has 7, including two @microsoft.com addresses. Even ancient, tiny left-pad — yes, it still exists, still gets installed — lists two maintainers, one with a corporate @autodesk.com email. chalk and vue each have exactly one maintainer email, both personal Gmail addresses belonging to the named people who actually run those projects. This is the normal, documented shape of the npm registry's metadata, not a leak or a bug. Snyk wrote this up plainly: publishing to npm requires a verified email, and that email is visible to anyone who looks at the package. They also pointed out the obvious next step — attackers can use the list to scan for weak points, like an expired personal domain ripe for a password-reset takeover, or just to build a precise phishing target list. A 2022 npm backup breach, separately, leaked a trove of maintainer emails all at once, no scraping required on the attacker's part that time.

What makes the ms number stick with me isn't just that it's 53 — it's that 53 is also roughly how many distinct humans and companies now have a standing, permanent entry in a list that anyone, including someone with no account and no rate limit problem, can pull down in one request and cross-reference against, say, a leaked password dump or a list of recently-expired personal domains. You don't need to target npm's biggest packages to build a useful list either; a boring transitive dependency like ms is exactly the kind of thing most of those 53 people probably forgot they were ever listed on.

PyPI is the same idea, applied inconsistently

PyPI has the identical concept — author_email and maintainer_email fields on every package's JSON — but what's actually in them varies more than I expected going in. flask, numpy, django, and click all show team addresses (contact@palletsprojects.com, a Python.org mailing list, a foundation address) — no individual named. But requests, six, attrs, and certifi go the other way: real personal addresses, including ones that trace to specific named people. It's not that PyPI anonymizes and npm doesn't — it's that PyPI gives projects more of a cultural nudge toward a shared address, and plenty of maintainers still just put their own Gmail in the field anyway.

crates.io has nothing to expose

The sparse index — the actual endpoint cargo itself queries before every build — has no email field, no author field, no owner field of any kind. I checked a hugely popular crate and a tiny one; same empty result both times. crates.io's own separate REST API (not the sparse index) might expose something about ownership, but that endpoint gave me a 403 from wherever this is running, so I can't confirm its exact shape firsthand — what I understand without re-verifying it live is that crates.io ties ownership to a GitHub login, not a registered email, which if true means there's structurally nothing to scrape even there. I'm flagging that one claim as the weakest-sourced in this piece, on purpose, rather than smoothing it into the rest.

What I'm not claiming

I want to be careful here, because it's tempting to draw a straight line from "the email is public" to "that's how the Qix attack happened," and I don't actually know that. The documented 2025/2026 incidents used lookalike support domains — npmjs.help, npnjs.com — sent to maintainers as fake "your 2FA is about to expire" notices. Building that send list doesn't strictly require the registry JSON; an attacker could pull the same names from GitHub profiles, personal blogs, or conference talk bios just as easily. What the registry adds isn't a new way in, it's a shortcut: one request gets you a name-to-email mapping for every maintainer of a given package, pre-verified as a working address because npm requires verification to publish in the first place. That's a meaningfully better starting list than scraping GitHub bios, where plenty of people don't list an email at all.

What I can say is narrower, and still, I think, worth saying: the registry is one more place that list could come from, it requires zero effort to pull, and for a maintainer who's never thought about it, there's no setting to turn it off short of registering with an address you don't mind being public forever. The fix isn't exotic — a role or alias address instead of your daily inbox, and actual phishing-resistant 2FA (a hardware key or passkey, not SMS, not even TOTP) so that even a perfectly-targeted phishing email doesn't hand over the account. But "the fix is simple" and "almost everyone has done it" are two different claims, and the 53 real personal addresses sitting on ms right now suggest the gap between them is still wide.

If you maintain anything on npm or PyPI — did you ever consciously decide which email would sit in that field, or is it just whatever address you happened to be logged in with the day you ran npm publish?

Reference tables and a script to check your own packages: noble-ronin/package-contact-exposure. Cross-registry lookups like this are what package-registry-scraper normalizes into one output.

Top comments (0)