Back to glossary

npm Registry

The npm registry is the public database at registry.npmjs.org that stores every npm package's metadata and tarballs and serves them to package managers.

Also known as npmjs registry, registry.npmjs.org, node registry, public npm registry, npm package registry

The npm registry, also called the npmjs or node registry, is the public package database at registry.npmjs.org that stores every published version as an immutable tarball plus JSON metadata, and serves both to npm, pnpm, Yarn, and Bun. It authenticates publishers, not code: whatever a valid credential publishes stays installable until someone removes it.

Removal is the npm registry's real security boundary, and it is slow and quiet. The registry keeps a timestamp for every version it ever accepted, including versions it later deleted, so its own metadata records how long bad code stayed live. We read those timestamps on October 30, 2026. The malicious plain-crypto-js@4.2.1 behind the axios compromise was installable for 3 hours 26 minutes. Packages from the Beamglea phishing campaign lasted a median of 15 days. Seventeen more packages carrying the same Beamglea redirect script are still installable a year later.

Our position: treat the npm registry as a distribution network with a reactive abuse desk, not as a vetting layer. Publish-time malware scanning, live since July 28, 2026, narrows the gap without changing the model. The decision about what runs on your machine still has to be made at install time, from what is inside the tarball.

What is the npm registry?

#

The npm registry is an HTTP API that maps package names to metadata documents and tarballs. npm's documentation calls it "a database of JavaScript packages, each comprised of software and metadata." The npm CLI is one client. pnpm, Yarn, and Bun read the same API, so a policy the registry enforces protects every client, while a setting in one client protects only that client's users.

The registry was designed in 2010 to make publishing nearly frictionless. Older npm documentation describes it as "powered by a CouchDB database," and the CouchDB heritage still shows. Every package is one JSON document, every document carries a _rev revision, and anyone with an account can create a new name without human review.

GitHub has operated the npm registry since March 2020, when it announced the npm acquisition and described npm as "home to over 1.3 million packages with 75 billion downloads a month." The API sits behind Cloudflare. Every response we fetched carried server: cloudflare and cache-control: public, max-age=300, so a metadata change can take up to five minutes to reach clients reading through the CDN.

"Node registry" is not a separate service. Node.js does not run a package registry. People searching for the node registry mean registry.npmjs.org, the default value of npm's registry setting.

How many packages are on the npm registry?

#

The npm registry holds about 4.3 million installable package names, by our count on October 30, 2026. npm does not publish an official total, and the registry root returns an empty object:

Bash
$ curl -s https://registry.npmjs.org/
{}

We started from all-the-package-names, a package that lists every public name and republishes daily. Version 2.0.2609, published at 00:14 UTC on October 30, 2026, lists 4,618,847 names. Of those, 1,831,822, or 39.7%, are scoped:

Bash
$ curl -sL https://registry.npmjs.org/all-the-package-names/-/all-the-package-names-2.0.2609.tgz \
    | tar xzO package/names.json \
    | jq 'length, (map(select(startswith("@")))|length)'
4618847
1831822

A name list overcounts, because a name survives after its code is gone. We fetched the packument for a random sample of 2,000 names to see what each one serves today:

State on Oct 30, 2026Names in sampleShareProjected across 4.62M names
Live, with installable versions1,87693.8%about 4.33 million
Security holding package (0.0.1-security)633.2%about 145,000
Unpublished tombstone, no versions371.9%about 85,000
HTTP 404241.2%about 55,000

The 95% confidence interval on the live share is about ±1.1 points, which puts the live count between roughly 4.28 and 4.38 million. Growth has been steep. npm's official statistics showed more than 2.5 million live packages at the end of 2023, and our 2023 npm retrospective counted 10,518,566 versions published and 1,241,583 unpublished that year alone. "Live" does not mean "legitimate." The live share includes spam, abandoned names, and packages nobody has reviewed.

How the npm registry stores a package

#

The npm registry stores each package as one metadata document, called a packument, plus one tarball per version. Four packument fields decide what clients install:

  • versions maps each version string to its manifest, including dist.tarball, dist.integrity, and the registry's signature.
  • dist-tags maps labels such as latest to versions. A bare npm install semver resolves latest.
  • time records when each version was published. Entries for deleted versions stay in this map.
  • _npmUser on each version records which identity published it.

Clients usually request the abbreviated form with Accept: application/vnd.npm.install-v1+json, which keeps only the fields needed to install. For axios on October 30, 2026, the full packument was 868,092 bytes and the abbreviated one 305,314 bytes. The abbreviated form drops time, so the forensic record described below is invisible to a normal install.

Smaller endpoints expose the same data. We ran these against the live registry:

Bash
$ curl -s https://registry.npmjs.org/-/package/semver/dist-tags
{"legacy":"5.7.2","latest-6":"6.3.1","latest":"7.8.5"}

$ curl -s https://registry.npmjs.org/semver/7.8.5 | jq -c ._npmUser
{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:7a0c479c-0252-4ccd-81e5-c43546a76551"}}

The second response shows that semver 7.8.5 was published through trusted publishing from GitHub Actions, not from a maintainer's laptop. That field records how a version was published, not whether its code is safe. In the TanStack compromise, attackers published through the project's legitimate trusted-publisher binding.

Registry data is immutable at the version level. npm's unpublish policy states: "Once package@version has been used, you can never use it again." That rule is why a lockfile's integrity hash means something. The bytes behind a name and version can be deleted, but never swapped. Everything around them is mutable: dist-tags move, versions get deprecated or removed, and npm can rebuild the whole document.

Figure 1 in text. A new version first passes publish-time scanning, which GitHub says typically takes about five minutes and can take 15 minutes or more. It then becomes live: it appears in versions and time, its tarball returns HTTP 200, and ranges and latest resolve to it. From there a version can reach three end states. A deprecated version stays in versions and installs, and npm prints the deprecation message. An unpublished package becomes a tombstone: the full packument keeps time.unpublished with the removed version list, the abbreviated packument returns only name and modified, and npm install fails with ENOVERSIONS. A package removed by npm becomes a security holding package: npm deletes the versions, their tarballs return HTTP 404, a 0.0.1-security placeholder owned by the npm account takes over latest, and the old publish dates stay in time. npm install <name> then succeeds and installs an empty placeholder.

What happens when npm removes a malicious package?

#

When npm removes a malicious package, it deletes the bad versions and their tarballs and publishes a placeholder called a security holding package. A malware advisory then appears in the GitHub Advisory Database. The axios compromise in March 2026 shows each step. We fetched the packument for plain-crypto-js, the dependency the attacker injected into axios@1.14.1 and axios@0.30.4:

Bash
$ curl -s https://registry.npmjs.org/plain-crypto-js | jq '{"dist-tags", versions: (.versions|keys), time}'
{
  "dist-tags": { "latest": "0.0.1-security.0" },
  "versions": [ "0.0.1-security.0" ],
  "time": {
    "created": "2026-03-31T04:26:32.907Z",
    "modified": "2026-03-31T04:26:35.731Z",
    "4.2.0": "2026-03-30T05:57:32.867Z",
    "4.2.1": "2026-03-30T23:59:12.278Z",
    "0.0.1-security": "2026-03-31T03:25:11.636Z",
    "0.0.1-security.0": "2026-03-31T04:26:33.055Z"
  }
}

The malicious 4.2.1 is gone from versions but still listed in time. Its tarball URL returns 404, and npm install plain-crypto-js@4.2.1 fails with "No matching version found." The first placeholder landed at 03:25:11 UTC, 3 hours 25 minutes 59 seconds after the malicious publish. The document's created timestamp is later than its own versions, which shows npm rebuilt the document rather than editing it. Socket's scanner flagged 4.2.1 at 00:05:41 UTC, six and a half minutes after publish.

A security holding package installs without error. With npm 10.9.9, npm install plain-crypto-js added 0.0.1-security.0, a 438-byte package described as "security holding package", and reported "found 0 vulnerabilities." The advisory for the package covers >=0, and under semver rules that range does not match a prerelease such as 0.0.1-security.0, so npm audit stays silent. The placeholder exists to stop anyone from re-registering the name, not to warn you. A project that silently resolves to it has lost a dependency without noticing.

The warning lives in the advisory feed. The bulk endpoint that npm audit calls returns a malware advisory for the removed version:

Bash
$ curl -s -X POST -H 'content-type: application/json' \
    https://registry.npmjs.org/-/npm/v1/security/advisories/bulk \
    -d '{"plain-crypto-js":["4.2.1"]}'
{"plain-crypto-js":[{"id":1115706,"url":"https://github.com/advisories/GHSA-2x9r-6wxq-hrr7",
  "title":"Malware in plain-crypto-js","severity":"critical","vulnerable_versions":">=0",
  "cwe":["CWE-506"],"cvss":{"score":0,"vectorString":null}}]}

The advisory is rated critical, with a CVSS score of 0 and no CVE ID. Malware does not get a CVE, which is why scanners that only match CVEs miss it. The advisory text is blunt: "Any computer that has this package installed or running should be considered fully compromised."

The holding-package convention is metadata, and metadata can be wrong. In June 2026, an npm tooling bug marked one-character packages as security holders. It moved latest on names such as c, r, and x to 0.0.1-security until npm reverted the change the same day.

Owners can also remove their own packages. npm allows an unpublish within 72 hours of publishing. After that, a package qualifies only if no other public package depends on it, it had fewer than 300 downloads in the last week, and it has a single owner. The resulting tombstone does not say who unpublished the package or why. One tombstone from our sample, reply-mquery, returns this abbreviated document, identical in shape to any other:

JSON
{"name":"reply-mquery","modified":"2026-03-01T19:42:35.271Z"}

How long do malicious packages stay on the npm registry?

#

Malicious and abusive packages stay on the npm registry for hours to more than a year, depending on when someone notices them. Because time keeps entries for deleted versions, the gap between the first bad publish and the first 0.0.1-security placeholder measures the exposure window. We computed it from live packuments on October 30, 2026.

Figure 2 in text, as a table:

CaseFirst publishFirst placeholderInstallable for
plain-crypto-js@4.2.1, axios compromiseMar 30, 2026, 23:59 UTCMar 31, 2026, 03:25 UTC3 h 26 min (Socket flagged it at 6 min 29 s)
crossenv, typosquat of cross-envJul 19, 2017Aug 1, 201713 days 11 hours
Beamglea-pattern phishing packages, 171 namesSep 15 to Oct 8, 2025Oct 8 to 9, 2025median 15.3 days, range 1.5 to 24.1 days
Beamglea packages published after the takedown, 17 namesOct 8 to 23, 2025nonestill installable, 372 days or more
Spam names in our random sample, 31 namesMay to Jul 2024Nov 12 to 13, 2025median 545 days, range 489 to 554 days

crossenv is the textbook typosquatting case. It sent environment variables to a remote host at install time, and npm's 2017 write-up said "the intent was to collect useful data from tricked users." Its packument still lists 15 publish timestamps from July 19, 2017, for versions that no longer exist.

The Beamglea packages did not run on install. As we documented in October 2025, the campaign used the registry as free hosting: package CDNs serve any public npm file over HTTPS, so a package named redirect- plus six random characters became a phishing redirector without the attacker running a server. We downloaded the 17 surviving tarballs. Every one ships a beamglea.js redirect script, and every one embeds a target's email address in its redirect URL. We are not naming them here, because they still work.

The 31 spam names make up about half of the holding packages in our random sample. They follow the generated naming style of the TEA protocol spam waves, whose packages npm took down in mid-November 2025. Spam is not malware, but the cleanup shows the same pattern: the registry acts in bulk, after outside researchers raise the alarm.

The windows explain what a dependency cooldown can and cannot do. A one-day minimum release age would have kept plain-crypto-js@4.2.1 out of every fresh resolution. A seven-day window would still have let in crossenv and most Beamglea packages, which stayed up longer. A cooldown protects you only when someone else finds and removes the version before your window closes.

npm registry security controls compared

#

The npm registry has added controls on both sides of a publish since 2016. Each one closes a specific path, and each leaves a gap that a recent attack walked through.

ControlSinceWhat it stopsWhat it misses
Unpublish limitsMarch 2016Owners deleting packages that others depend onMalicious versions, which npm removes itself
Punctuation-only name ruleDecember 2017New names that differ from existing ones only by -, _, or .Other lookalikes, new brand names, and scope impersonation
Trusted publishing (OIDC)2025Stored publish secrets in CIA compromised workflow publishing through the real binding
Classic token revocationDecember 9, 2025Long-lived classic tokens harvested from CIGranular tokens and stolen sessions
Reset of granular tokens that bypass 2FAMay 19, 2026Tokens the Mini Shai-Hulud worm had already collectedTokens issued and stolen after the reset
Staged publishing (opt-in preview)May 20, 2026A publish going live without a human 2FA approvalEvery package whose owner has not turned it on
Publish-time malware scanningJuly 28, 2026Malware that matches what the scanner recognizesNovel payloads that pass the scan
Removal and security holding packageBy 2017Further installs of a known-bad versionEvery install during the exposure window

Our verdict: staged publishing is the only registry control that puts a human between a stolen credential and an installable version, and it should be the default for any package with meaningful downloads. The other controls each harden one step and leave the rest to chance.

The punctuation rule arrived in December 2017, months after crossenv. In npm's words, "you can no longer publish packages that differ from existing packages in punctuation only." It blocks crossenv-style names and nothing else. Trusted publishing, which npm rolled out to all users in 2025, removes stored secrets but trusts whatever the workflow builds. The classic token revocation and the reset of 2FA-bypass granular tokens, which followed the Mini Shai-Hulud wave against @antv, cut off credentials attackers had already collected. Staged publishing entered public preview the next day in npm CLI 11.15.0, and it is still opt-in per package.

Publish-time scanning has the clearest limit. GitHub's changelog says each new version may be "published as normal, held for manual review, or blocked." One week after launch, on August 4, 2026, the keyv and cacheable compromise shipped a malicious preinstall hook through the registry anyway. None of these controls inspects what the code does once it is installed.

Registry configuration, scopes, and proxies

#

npm installs from the URL in its registry setting, which defaults to https://registry.npmjs.org/, unless a package's scope is mapped elsewhere. Scoped mappings in .npmrc route individual namespaces to private registries:

TOML, also INI
registry=https://registry.npmjs.org/
@acme:registry=https://npm.acme.internal/

Unscoped internal names are where this goes wrong. Suppose an internal package called acme-billing is unscoped and the public registry also has a package by that name. A client or proxy that checks both sources can pick the public one, which is dependency confusion. Scope every internal package and map the scope explicitly.

The registry setting is also where a package firewall plugs in: a proxy between your clients and registry.npmjs.org that decides, version by version, whether to serve a package. That moves the install decision off the public registry's removal clock. Since early 2026, npmjs.com package pages have also linked to Socket's analysis of each package, which puts a code-level review one click from the registry's own page.

What the npm registry should change next

#

The npm registry already records exposure windows, but only in a field normal installs never request. Removal leaves no machine-readable reason and no link from the placeholder to its advisory. A tombstone looks the same whether an owner cleaned up a test package or npm pulled malware. We think every removal should add a removed record to both the full and abbreviated packuments, with a timestamp, a reason code, and the advisory ID. Clients could then explain a removal, instead of failing with a bare 404 on a pinned tarball or quietly resolving an unpinned range to an empty placeholder with zero vulnerabilities reported.

The deeper problem is the design goal. The registry was built to make publishing frictionless, and it still accepts whatever an authenticated publisher sends. Seventeen packages carrying the redirect script from a campaign we documented a year ago are still being served. Until removal is fast and complete, the registry's verdict on a package is the last one to arrive, so yours has to come first.