The OpenSSF malicious packages dataset logged 192,730 malicious package reports in 2025, and 99.3% of them were on npm. That headline hides how lumpy the data is: two months, August and November 2025, account for 92.7% of npm's total. Strip those two spikes out and npm still carries about 91% of reports, PyPI is a distant second, and every other registry is in single or double digits.
Counts of malicious uploads tell you where attackers are publishing. They do not tell you where you were exposed. The incidents that actually reached production builds in 2025 and 2026 were a small number of account takeovers and worms on popular packages, not the hundreds of thousands of spam uploads that dominate the totals. This post puts the public datasets side by side, explains why they disagree, walks through the incidents that registries and maintainers documented themselves, and ends with the controls that measurably shrink exposure.
Key takeaways
- The public number for 2025 is 192,730 reports. That is the sum of monthly published reports in the OpenSSF malicious packages stats file, downloaded on 23 October 2026. One software supply chain vendor's 2026 report counts more than 454,600 new malicious packages for 2025 using its own detection. Both say npm dominates; they disagree by more than a factor of two on size.
- One or two campaigns can make a year. In OSV data, npm logged 142,163 reports in November 2025 and 35,099 in August 2025. Every other 2025 month sat between 478 and 5,579. In 2026, RubyGems jumped from 129 reports for all of 2025 to 3,369 by October, and 2,540 of those arrived in July.
- The incidents that hurt were takeovers, not typosquats. The chalk and debug compromise on 8 September 2025, the Nx compromise on 26 and 27 August 2025, and the Shai-Hulud worm from mid September 2025 all pushed malicious code into packages people already trusted and installed by default.
- Malicious versions of popular packages were live for hours. The chalk maintainers say the bad versions were up for about two hours. The Nx advisory shows versions live between about two and twelve hours. Low traffic typosquats last far longer: two malicious crates sat on crates.io for about 122 days.
- Registries changed the rules. npm revoked every classic token on 9 December 2025. PyPI reports more than 50,000 projects on trusted publishing. NuGet cut new API keys to 30 days from 17 August 2026. These changes target the account takeover path that produced the worst incidents.
- What you can measure is your own exposure. Lockfiles, install script policy, release cooldowns and an SBOM you can query by package and version decide whether a two hour malicious window reaches your builds and how fast you can answer "were we affected".
How many malicious open source packages were reported in 2025?
There is no single authoritative count. There are at least three kinds of numbers in circulation, and they measure different things.
Registry statements describe what a registry removed or acted on. They are the most authoritative and the least complete. GitHub said it removed more than 500 compromised packages in response to Shai-Hulud in its post of 22 September 2025. PyPI's 2025 year in review says its team processed more than 2,000 malware reports in 2025. Neither is a full census of malicious uploads.
The OpenSSF malicious packages dataset is a public, OSV format collection of reports contributed by researchers, registries and automated analysis feeds. Records carry MAL- identifiers and flow into osv.dev. The project publishes a daily stats file of published reports per ecosystem per month. This is the best public baseline because anyone can download it and reproduce the count.
Vendor reports use proprietary detection and often include spam, token farming packages and potentially unwanted applications. One software supply chain vendor's 2026 open source malware report counts more than 454,600 new malicious packages in 2025 and more than 1.233 million cumulatively, and states that over 99% of open source malware occurred on npm. The same vendor's category breakdown puts 55.9% of 2025 logged packages under "repository abuse" and 27.5% under potentially unwanted applications. Only 3.9% were secrets exfiltration and 2.7% droppers or loaders.
That category split matters more than the headline. On that vendor's own breakdown, fewer than one in ten of its 2025 malicious packages did the kind of thing most security teams picture: stealing credentials, dropping a second stage or opening a backdoor.
Table 1: OSV malicious package reports by ecosystem, 2024 to 2026
Source: OpenSSF malicious packages stats file (all.csv), last updated 23 October 2026 at 00:55 UTC. Values are sums of monthly published reports. Rows with a placeholder month of 0001-01-01 are excluded. 2026 runs from 1 January to 23 October.
| Ecosystem (OSV label) | 2024 | 2025 | 2026 to 23 Oct | Share of 2025 | Share of 2026 so far |
|---|---|---|---|---|---|
| npm | 8,317 | 191,307 | 13,031 | 99.26% | 72.49% |
| PyPI | 2,645 | 1,245 | 1,520 | 0.65% | 8.46% |
| RubyGems | 791 | 129 | 3,369 | 0.07% | 18.74% |
| NuGet | 738 | 22 | 22 | 0.01% | 0.12% |
| VSCode, open-vsx.org | none listed | 13 | 4 | 0.01% | 0.02% |
| Go | none listed | 8 | 10 | under 0.01% | 0.06% |
| crates.io | none listed | 1 | 18 | under 0.01% | 0.10% |
| Maven | none listed | 2 | 0 | under 0.01% | 0% |
| VSCode, no registry suffix | none listed | 2 | 2 | under 0.01% | 0.01% |
| Packagist | none listed | 0 | 1 | 0% | 0.01% |
| Git | none listed | 1 | 0 | under 0.01% | 0% |
| All ecosystems | 12,491 | 192,730 | 17,977 | 100% | 100% |
Two things stand out. First, the 2025 total is fifteen times the 2024 total, and almost all of that growth is npm. Second, PyPI, NuGet and RubyGems all fell sharply in 2025 in this dataset: PyPI by 52.9%, NuGet by 97.0% and RubyGems by 83.7% against 2024. You may have seen claims that PyPI and NuGet malicious uploads fell by 43% and 60% after stronger authentication. We could not find a primary source for those percentages, and the OSV data above shows different numbers, so treat them as unconfirmed.
A fall in reports is not proof of a fall in attacks. It can also mean a feed stopped contributing, a registry got faster at removing packages before anyone filed a report, or researchers shifted attention to npm. We return to this under normalisation.
Table 2: three ways to count 2025
| Measure | 2025 figure | Who counts | What it includes | What it misses |
|---|---|---|---|---|
| OSV published reports, all ecosystems | 192,730 | OpenSSF dataset, many contributors | Reports with a MAL- record, deduplicated by package | Packages removed before anyone reported them; anything not contributed |
| Vendor new malicious packages | more than 454,600 | One supply chain vendor, proprietary detection | Spam, token farming, potentially unwanted apps, malware | Independent verification; method not public |
| PyPI malware reports processed | more than 2,000 | PyPI admins | Reports PyPI acted on | Uploads blocked or removed without a report; other registries |
| Shai-Hulud packages removed by npm | more than 500 | GitHub, 22 Sep 2025 | Compromised packages removed in response to one worm | Later waves; other campaigns |
The honest summary for a board slide is a range with a label: public datasets logged roughly 190,000 reports in 2025, a vendor using wider criteria counted more than 450,000, and more than nine in ten of either count were on npm.
Why does npm account for the vast majority of malicious uploads?
Several structural reasons point the same way, and the data supports each of them.
Publishing is cheap and fast. A new npm account can publish a package within minutes. Mass spam campaigns exploit that. One supply chain vendor's report attributes 169,538 packages to a single npm campaign it calls IndonesianFoods. The OSV monthly series shows 142,163 npm reports in November 2025 alone. We have not matched those records to that campaign, so treat the overlap as likely rather than proven.
Install scripts run by default. npm runs preinstall, install and postinstall scripts unless you turn them off. That gives a malicious package code execution on a developer laptop or CI runner at install time, before any of your code runs. CISA's 23 September 2025 alert on Shai-Hulud describes a worm that used exactly this path to harvest tokens and republish itself. The Nx advisory describes a postinstall payload. Coverage of the November 2025 wave describes a move to preinstall.
Dependency trees are deep and popular packages are tiny. The ten packages listed in the chalk issue are small terminal utilities such as ansi-regex, strip-ansi and supports-color. They sit deep inside build tools, so a single compromised maintainer account reaches a large share of JavaScript projects without anyone choosing to install those packages directly.
Token based publishing was the norm. Until npm's changes in late 2025, long lived classic tokens could publish from anywhere. A stolen token was a publish credential. The worms of 2025 and 2026 were, at heart, token theft loops.
Researchers watch npm hardest. A large share of the OSV feed comes from automated analysis that scans new npm uploads. More scanning produces more reports. This inflates npm relative to registries with less automated coverage, which is one reason the per ecosystem shares should be read as "where attackers and researchers both spend time" rather than "where risk lives".
What happened in the September 2025 chalk and debug account takeover?
The maintainer record is the GitHub issue opened on the chalk repository on 8 September 2025 and titled "Version 5.6.1 published to npm is compromised". It lists the compromised versions in that maintainer's packages, including chalk@5.6.1, ansi-styles@6.2.2, supports-color@10.2.1, strip-ansi@7.1.1, ansi-regex@6.2.1, wrap-ansi@9.0.1, slice-ansi@7.1.1, has-ansi@6.0.1, chalk-template@1.1.1 and supports-hyperlinks@4.1.1. A sibling issue on the debug repository covered debug. A chalk maintainer reported the malware to npm, published clean follow up versions (for example chalk@5.6.2), and wrote that the compromised account had been removed from the packages. The issue notes that npm took down the infected versions and that they were live for "approximately 2 hours".
Vercel's incident response post of the same day gives the clearest downstream data we found. It describes 18 compromised npm packages, says it activated incident response at 17:39 UTC, purged build caches at 22:19 UTC, and found 70 teams with builds that included compromised versions across 76 unique projects. The payload, per Vercel, ran in browsers when bundled into web apps and redirected cryptocurrency and web3 wallet transactions to attacker addresses.
How the account was compromised. The chalk issue itself does not confirm the method. Security researchers widely reported that the maintainer was phished through a lookalike npm support domain. We treat that as reported, not confirmed by the maintainer page we fetched.
How big was the footprint? Security researchers reported that the affected packages had roughly 2 billion to 2.6 billion combined weekly downloads. We could not confirm the combined figure on npm's own pages, and the published estimates differ, so we give it as a range from secondary reporting.
The useful lesson is about scale versus time. The download footprint was enormous, but the window was about two hours, the payload targeted browser wallet flows, and the downstream data from one large hosting platform shows 76 projects affected. Teams with lockfiles and no automatic upgrades during that window were not exposed at all.
For a deeper incident walkthrough, see our npm incident history from left-pad to Shai-Hulud.
How did the Shai-Hulud worm waves work, and what do primary sources confirm?
Wave one, September 2025
GitHub says it was notified of Shai-Hulud on 14 September 2025. In its post of 22 September, GitHub describes a worm that spread through compromised maintainer accounts by injecting malicious post-install scripts into popular packages, and that stole several kinds of secrets, not only npm tokens. GitHub says it removed more than 500 compromised packages and blocked uploads that contained the worm's indicators of compromise, which cut off the self-replicating pattern.
CISA's alert of 23 September 2025 confirms the core mechanics in plain terms. The worm scanned for credentials, targeted GitHub personal access tokens and cloud keys for AWS, Google Cloud and Azure, exfiltrated them, uploaded them to a public GitHub repository named "Shai-Hulud", then authenticated to npm as the compromised developer and published infected versions of that developer's other packages. CISA's recommendations included checking lockfiles and nested dependencies, searching artifact caches, pinning npm dependencies to releases published before 16 September 2025, rotating all developer credentials and requiring phishing resistant MFA on developer accounts.
The Nx compromise that preceded it
The Nx security advisory (GHSA-cxm3-wv7p-598c) is one of the most precise primary records of 2025. It describes a GitHub Actions workflow vulnerable to injection through a pull request title, running under pull_request_target with elevated permissions, which the advisory believes let an attacker obtain the npm publish token. Malicious versions of nx and several @nx packages were then published starting at 22:32 UTC on 26 August 2025. The payload, a postinstall script, searched the file system for credentials and posted them to a new public repository under the victim's own GitHub account. The advisory also notes that the Nx Console editor extension installed the latest nx package to check its version, which could trigger the payload just by opening an editor.
NuGet's own blog, writing in August 2026 about API key lifetimes, cites the Nx team's figure that the malicious package was activated 6,000 times in 36 minutes before it was taken down.
Wave two, November 2025
A second wave, widely called Shai-Hulud 2.0 or "The Second Coming", started around 21 to 24 November 2025. We did not find a GitHub or npm post with official counts for this wave. Security researchers' counts vary widely: one put it at 621 infected npm packages and about 25,000 compromised GitHub repositories, another at 600 to 800 packages, and another at more than 27,000 repositories. One supply chain vendor's 2026 report lists only 49 packages under that campaign name. PyPI posted on 26 November 2025 that it was not exploited, but that some long lived PyPI tokens were found exposed in compromised GitHub repositories and were revoked as a precaution.
These numbers disagree because they count different things: package names, versions, repositories and leaked secrets. If you need a single figure, quote the registry's own statement and name what it counts.
2026 descendants
Worm style compromises continued into 2026. OSV record MAL-2026-11773, published 4 August 2026, covers 12 versions of a scoped npm package credited to four independent finders, and is one entry among a wave that security researchers linked to a compromise of the keyv maintainer's account and sibling caching packages. Researchers reported that poisoned versions carried valid build provenance because they were published through the maintainer's own pipeline. Reported counts range from more than 400 packages to more than 868, which is why we do not state one. The OSV monthly series shows npm reports rising to 4,078 in August 2026, the highest npm month of 2026 so far.
For the full timeline of the original worm, see the Shai-Hulud npm worm timeline and lessons for defenders and our explainer. For the registry side, see npm takedown times during Shai-Hulud.
Timeline of registry documented incidents, 2025 to 2026
Table 3: incidents with primary records
| Date (UTC unless noted) | Ecosystem | Incident | Technique | Primary record |
|---|---|---|---|---|
| 20 to 28 Jul 2025 | RubyGems | Gems stealing social media credentials flagged on 20 Jul; nearly all removed and accounts terminated 23 to 28 Jul; 16 more removed after a researcher report on 7 Aug | Malicious new gems | RubyGems blog, 25 Aug 2025 |
| 26 to 28 Jul 2025 (EDT) | PyPI | Phishing via lookalike domain pypj.org; 4 accounts phished; num2words 0.5.15 and 0.5.16 carried malware and were removed | Account takeover by phishing | PyPI incident report, 31 Jul 2025 |
| 26 to 27 Aug 2025 | npm | Nx packages published with a credential stealing postinstall | Stolen publish token via CI workflow injection | Nx advisory GHSA-cxm3-wv7p-598c |
| 5 to 15 Sep 2025 | PyPI | Workflows modified to send PyPI tokens stored as GitHub secrets to external servers; affected tokens invalidated; no malicious packages published | CI secret theft | PyPI blog, 16 Sep 2025 |
| 8 Sep 2025 | npm | chalk, debug and related packages published with a wallet hijacking payload; live about two hours | Maintainer account takeover | chalk issue 656; Vercel post |
| 14 to 23 Sep 2025 | npm | Shai-Hulud worm; more than 500 packages removed | Token theft and self propagation | GitHub blog 22 Sep; CISA alert 23 Sep |
| 24 Sep 2025 | crates.io | faster_log and async_println deleted after about 122 days live | Typosquat with runtime payload | Rust blog, 24 Sep 2025 |
| Oct 2025, closed 21 Oct | Open VSX | Leaked publishing tokens used to publish malicious extensions; tokens revoked | Leaked token abuse | Eclipse Foundation, 27 Oct 2025 |
| 21 to 24 Nov 2025 | npm | Shai-Hulud second wave; PyPI revoked exposed tokens as a precaution | Token theft, preinstall execution (reported) | PyPI blog, 26 Nov 2025; counts secondary |
| 9 Dec 2025 | npm | All classic tokens revoked | Registry control | GitHub changelog, 9 Dec 2025 |
| Since Jan 2025, reported 8 Apr 2026 | npm, PyPI, Go, Rust, Packagist | More than 1,700 packages linked to a North Korea attributed campaign | Fake developer tooling loaders | Researcher findings via secondary coverage |
| 4 Aug 2026 | npm | Worm wave across a caching package family | Maintainer account takeover with valid provenance (reported) | OSV MAL-2026-11773; counts secondary |
What changed on PyPI and NuGet after 2FA and trusted publishing?
PyPI
PyPI's 2025 year in review, published 31 December 2025, reports:
- more than 52% of active users with non-phishable 2FA enabled
- more than 50,000 projects using trusted publishing
- more than 20% of all file uploads in the past year made through trusted publishers
- 17% of uploads in the past year carrying an attestation
- more than 2,000 malware reports processed, 66% handled within four hours and 92% within 24 hours
PyPI also added warnings for untrusted domains after the July phishing campaign and now checks periodically for expired maintainer email domains to prevent domain resurrection attacks.
Does this explain the fall in PyPI reports? Partly, at most. The OSV dataset shows PyPI reports falling from 2,645 in 2024 to 1,245 in 2025, then rising again to 1,520 in the first ten months of 2026. Trusted publishing protects existing projects from token theft. It does nothing to stop someone registering a new malicious project, which is the source of most malicious uploads. The 2025 PyPI incidents with real reach (the num2words phishing and the GitHub Actions token campaign) were exactly the account and token paths that 2FA and trusted publishing target, and in the token case PyPI says no malicious packages were published.
NuGet
NuGet launched trusted publishing in September 2025, letting GitHub Actions workflows publish with a short lived OIDC token in place of a stored API key. Its blog post of 3 August 2026 announced that new API keys are capped at 30 days from 17 August 2026, and that all keys created before that date expire on 1 November 2026. The stated reason is blunt: an API key is effectively a password for publishing packages.
In OSV data NuGet fell from 738 reports in 2024 to 22 in 2025 and 22 again in 2026 so far. That is a 97% drop, too large to credit to authentication changes that arrived in late 2025. A change in which feeds contribute NuGet reports is a more likely explanation, though the dataset does not record it.
npm
npm's changes after Shai-Hulud were the largest of any registry. GitHub's 22 September 2025 plan set the direction: publishing limited to local publishing with 2FA, granular tokens with short lifetimes, or trusted publishing; deprecation of classic tokens; and a move from TOTP to FIDO based 2FA. The 9 December 2025 changelog confirms that all classic tokens were permanently revoked, that npm login now issues two hour session tokens, that write tokens are capped at 90 days, and that new packages have 2FA enabled by default.
Table 4: registry publishing controls, as of October 2026
| Registry | Trusted publishing | Token limits | 2FA position | Source |
|---|---|---|---|---|
| npm | Yes, CI providers including GitHub Actions and GitLab | Classic tokens revoked 9 Dec 2025; write tokens up to 90 days; two hour login sessions | On by default for new packages; FIDO preferred | GitHub changelog, 9 Dec 2025; GitHub blog, 22 Sep 2025 |
| PyPI | Yes; more than 50,000 projects | Trusted publishing tokens are short lived and scoped | More than 52% of active users on non-phishable 2FA | PyPI 2025 year in review |
| NuGet | Yes, since Sep 2025 | New keys up to 30 days from 17 Aug 2026; older keys expire 1 Nov 2026 | Not covered in the sources we fetched | .NET blog, 3 Aug 2026 |
| Open VSX | Not covered | Shorter default token validity; faster revocation | Not covered | Eclipse Foundation, 27 Oct 2025 |
| RubyGems | Announced in 2023 per the RubyGems blog index | Not covered | Not covered | RubyGems blog |
| crates.io | Not covered in the sources we fetched | Not covered | Not covered | Rust blog |
Which techniques dominate per ecosystem?
The registry records point to a clear split between high volume, low impact uploads and low volume, high impact takeovers.
| Ecosystem | Dominant by volume | Highest impact documented, 2025 to 2026 | Execution point |
|---|---|---|---|
| npm | Spam and token farming packages; typosquats | Maintainer account takeover (chalk, debug, Nx, Shai-Hulud, 2026 worm waves) | preinstall and postinstall scripts; browser bundles |
| PyPI | New malicious projects, typosquats | Phishing account takeover (num2words); CI token theft attempt | setup.py on source installs; import time code |
| RubyGems | Bulk junk and malicious gems from new accounts | Credential stealing gems (Jul 2025) | Gem install hooks and runtime |
| crates.io | Rare typosquats | Wallet key stealers copying real crates | Runtime only; the Rust blog notes nothing ran at build time |
| Go | Rare; impersonated module paths | Loader modules in a state linked campaign | Runtime |
| VS Code and Open VSX | Malicious extensions | Leaked publisher tokens used to ship malware | Extension activation |
| NuGet, Maven | Very low report counts | No major 2025 to 2026 takeover in our primary sources | Build and runtime |
Dependency confusion, where an attacker publishes a public package that shadows a private one, is in scope for the OpenSSF dataset, but none of the registry posts we fetched documented a 2025 or 2026 case. It remains a configuration risk you can close yourself (see the walkthrough below).
For a deeper comparison of tactics, see comparing malicious package tactics across npm, PyPI, RubyGems and crates.io and our PyPI malicious packages 2025 report.
How did state linked campaigns spread across npm, PyPI, Go and Rust in 2026?
Researchers reported in April 2026 that a North Korea attributed campaign known as Contagious Interview had linked more than 1,700 malicious packages since January 2025. Coverage of that research names example packages across npm, PyPI, Go, Rust and Packagist. The packages posed as developer tooling, such as logging and licence helpers, and acted as loaders that fetched a second stage tailored to the victim's operating system. The researchers noted that the malicious code did not run at install time; it was hidden inside functions that matched the package's stated purpose.
That design matters for defenders. Install script policies, which stop most npm worms, do nothing against a loader that runs when your code calls Logger::trace. Cross ecosystem campaigns also show up thinly in per registry counts: OSV logged only 10 Go reports and 18 crates.io reports in 2026 to date, yet both ecosystems appear in the campaign. One supply chain vendor's 2026 report attributes more than 800 packages to a single state linked group, about 97% of them on npm.
We could not fetch the researchers' primary write up within this post's research window, so the 1,700 figure comes from secondary coverage citing them.
Why do single campaigns skew yearly totals, and how should you normalise?
Ranked table: npm OSV reports by month, 2025
Source: OpenSSF malicious packages stats file, 23 October 2026. Each block is roughly 5,000 reports, with a minimum of one block for any non zero month.
| Rank | Month | npm reports | Relative size |
|---|---|---|---|
| 1 | Nov 2025 | 142,163 | ████████████████████████████ (28) |
| 2 | Aug 2025 | 35,099 | ███████ (7) |
| 3 | Sep 2025 | 5,579 | █ |
| 4 | Oct 2025 | 1,414 | █ |
| 5 | Mar 2025 | 1,195 | █ |
| 6 | Dec 2025 | 1,067 | █ |
| 7 | Jul 2025 | 1,030 | █ |
| 8 | May 2025 | 962 | █ |
| 9 | Jun 2025 | 802 | █ |
| 10 | Feb 2025 | 775 | █ |
| 11 | Jan 2025 | 743 | █ |
| 12 | Apr 2025 | 478 | █ |
The median npm month in 2025 was about 1,049 reports. The largest month was 136 times that. When one month holds 74% of a year's reports, the annual total describes that month, not the year.
The same thing happened to RubyGems in 2026. It logged 2,540 reports in July 2026 and 611 in September 2026, against single digit or low double digit counts in most other months. Our OSV data note for September and October 2026 found that 608 of the 611 September RubyGems entries cited a single contributing source and were published within about seven hours on one day. Without those two months, RubyGems has 218 reports in 2026 so far, and npm's share of the 2026 total rises back to about 88%.
Ranked table: ecosystems by 2026 OSV reports, with and without spikes
| Rank | Ecosystem | Reports, 1 Jan to 23 Oct 2026 | Without the two largest months |
|---|---|---|---|
| 1 | npm | 13,031 | 6,897 |
| 2 | RubyGems | 3,369 | 218 |
| 3 | PyPI | 1,520 | 1,086 |
| 4 | NuGet | 22 | not meaningful |
| 5 | crates.io | 18 | not meaningful |
| 6 | Go | 10 | not meaningful |
| 7 | Open VSX | 4 | not meaningful |
With spikes removed, PyPI and RubyGems swap places. That is the normalisation point in one line: the ranking depends on whether you count campaigns or count months.
How to normalise in practice
- Use the median month, not the annual sum, when comparing ecosystems or years. It is resistant to single campaign spikes.
- Separate volume from reach. A count of malicious package names says nothing about downloads. A single takeover of a package with billions of weekly downloads outweighs a hundred thousand spam packages nobody installed.
- Read the contributing source. OSV records cite their finders. A spike from one source on one day is a bulk import, not a sudden attack surge.
- Weight by your own dependency graph. The only normalisation that matters for a security programme is exposure: how many of your projects resolved a malicious version during its live window.
- State what is counted. Packages, versions, repositories and secrets are different units. Never mix them in one chart.
We cover the npm case in more depth in how one npm campaign skews malicious package counts.
How long were malicious versions live before removal?
Table 5: documented exposure windows
| Incident | Live window | How the window is measured | Source |
|---|---|---|---|
| chalk and related packages, 8 Sep 2025 | About 2 hours | Maintainer statement | chalk issue 656 |
nx 21.5.0 | About 4 hours 12 minutes | Publish 22:32 UTC 26 Aug to removal 02:44 UTC 27 Aug | Nx advisory |
nx 20.12.0 | About 2 hours 7 minutes | Publish 00:37 to removal 02:44 UTC 27 Aug | Nx advisory |
@nx/key, @nx/enterprise-cloud 3.2.0 | About 11 hours 48 minutes | Publish 22:32 UTC 26 Aug to removal 10:20 UTC 27 Aug | Nx advisory |
PyPI phishing domain pypj.org | At least about 47 hours | First user report to registrar hold | PyPI incident report |
| RubyGems credential stealers, Jul 2025 | About 3 to 8 days | Flagged 20 Jul to removal 23 to 28 Jul | RubyGems blog |
| PyPI malware reports, 2025 | 66% within 4 hours; 92% within 24 hours | Report to action | PyPI year in review |
faster_log, async_println | About 122 days | Publish 25 May to deletion 24 Sep 2025 | Rust blog |
The pattern is consistent. Takeovers of popular packages get noticed fast, because thousands of people and automated systems are watching those packages, and they come down in hours. Low traffic typosquats can sit for months because nobody is looking. The two crates had 7,181 and 1,243 downloads and no dependent crates on crates.io.
That has a direct consequence for controls. A release cooldown of a few days would have kept every high profile takeover in this table out of your builds. It would not have helped against a typosquat you chose to install, because by the time you installed it, it was months old.
What controls reduce exposure?
These are the controls that map directly onto the techniques above. None of them depends on detecting malware.
| Control | Stops or limits | Does not help with | Effort |
|---|---|---|---|
Commit lockfiles and install with npm ci, pip install --require-hashes, cargo build --locked | Silent upgrades to a malicious version during its live window | A malicious version you deliberately add | Low |
| Release cooldown (minimum release age) | Hour scale takeover windows | Old typosquats | Low |
| Disable install scripts by default, allow per package | npm and Ruby install time payloads, worm propagation | Runtime loaders | Medium |
| Trusted publishing for your own packages | Token theft leading to malicious publishes under your name | Phished maintainers using interactive login | Medium |
| Phishing resistant MFA on registry and Git accounts | Lookalike domain phishing | Stolen CI tokens | Low |
CI workflow hardening (no untrusted input in run:, avoid pull_request_target with secrets) | Nx style token theft | Compromise of a dependency | Medium |
| Scoped registries and source mapping | Dependency confusion | Public package takeovers | Medium |
| Queryable SBOM inventory | Answering "were we exposed to version X" in minutes | Prevention | Medium |
| Policy gates on known bad versions | Reintroduction after an advisory exists | The first hours before an advisory | Low once SBOMs exist |
Step by step: lock down an npm project
- Commit the lockfile and install from it in CI.
npm ci
- Turn install scripts off by default for the project.
npm config set ignore-scripts true --location=project
Then rebuild only the packages that genuinely need native build steps, after review.
npm rebuild <package-you-trust>
- Check a specific package and version across the tree after an advisory.
npm ls chalk --all
npm ls debug --all
- Verify registry signatures and provenance attestations for what you installed.
npm audit signatures
- Add a release cooldown. In pnpm, recent versions support a minimum release age in
pnpm-workspace.yaml:
minimumReleaseAge: 4320 # minutes, three days
For automated update pull requests, Dependabot supports a cooldown block:
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
cooldown:
default-days: 3
Check your tool's current documentation for the exact option name in your version.
- Pin to a date during an active incident. CISA advised pinning to releases before 16 September 2025 during Shai-Hulud. npm's
--beforeflag resolves versions as of a date:
npm install --before="2025-09-15"
Step by step: Python, Go, Rust and .NET
Python with hashes:
pip-compile --generate-hashes requirements.in
pip install --require-hashes -r requirements.txt
Go with a read only module graph and checksum verification:
go env -w GOFLAGS=-mod=readonly
go mod verify
go env -w GOPRIVATE=example.com/yourorg/*
Rust with a locked graph:
cargo build --locked
cargo tree -i suspicious_crate_name
.NET with lock files and package source mapping to prevent dependency confusion, in nuget.config:
<packageSourceMapping>
<packageSource key="nuget.org">
<package pattern="*" />
</packageSource>
<packageSource key="internal">
<package pattern="YourOrg.*" />
</packageSource>
</packageSourceMapping>
and in the project file:
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
Step by step: harden the publishing side
If you publish packages, the takeover path runs through your accounts and CI.
# GitHub Actions: publish with OIDC trusted publishing, no stored token
permissions:
id-token: write
contents: read
jobs:
publish:
runs-on: ubuntu-latest
environment: release # require a human approval
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
registry-url: "https://registry.npmjs.org"
- run: npm ci
- run: npm publish --provenance
PyPI's November 2025 guidance adds a human approval step through GitHub Environments and auditing workflows for injection with static analysers. The Nx advisory is the case study for why: an attacker controlled pull request title reached a shell in a privileged workflow.
Step by step: answer "were we exposed" with an SBOM
When an advisory names a package and version, the question is which of your projects resolved it. Generate SBOMs per repository and image, then search them.
safeguard scan --dir .
safeguard scan --image registry.example.com/web:latest
safeguard scan --sbom ./sbom.cdx.json
Results are written as JSON under ./output. In the Safeguard platform, you can see every project that contains a given component and version, which turns a lockfile hunt across hundreds of repositories into a single lookup. Our post on SBOM incident response walks through that flow, and the compromised dependency playbook covers what to do next.
To be clear about limits: Safeguard does not detect new malicious packages and makes no claim to. It matches the components you run against vulnerability and advisory data once records exist, shows where an affected version lives, and lets policies and gates stop that version shipping again.
Common mistakes
- Quoting a vendor count as a registry fact. Vendor malware totals use proprietary detection and include spam. Label them, and pair them with a public baseline such as OSV.
- Comparing annual totals across ecosystems. One campaign can be 74% of a year. Use medians and say which spikes you removed.
- Mixing units. Packages, versions, repositories, downloads and leaked secrets are different counts. Shai-Hulud second wave figures differ by two orders of magnitude for this reason.
- Treating weekly downloads as victims. A two hour window on a package with billions of weekly downloads touched far fewer builds, as Vercel's 76 affected projects show.
- Relying on install script blocking alone. It stops npm worms. It does not stop loaders that run when your code calls them, the technique used in the 2026 state linked campaign and the malicious crates.
- Forgetting caches. CISA explicitly told teams to search artifact repositories. Vercel purged build caches. A clean lockfile does not clean a cached tarball.
- Rotating only npm tokens. The worms stole GitHub, cloud and CI credentials too. Rotate everything that was readable from the build environment.
- Assuming provenance means safe. The 2026 worm wave reportedly published with valid provenance from the maintainer's own pipeline. Provenance tells you where a package was built, not whether the person who triggered the build was legitimate.
- Skipping dormant accounts. PyPI's phishing report recommends removing dormant accounts and enabling 2FA on older accounts. Old maintainers on popular packages are a target.
FAQ
How many malicious packages were found in 2025?
The public OpenSSF malicious packages dataset logged 192,730 reports in 2025 across all ecosystems, based on its stats file downloaded on 23 October 2026. One vendor using wider criteria counted more than 454,600. Both agree that npm accounted for more than 99%.
Which package ecosystem has the most malware?
npm, by a wide margin. In OSV data it held 99.26% of 2025 reports and 72.49% of 2026 reports to 23 October. Even with its two largest 2025 months removed, npm still had about 91% of that year's reports.
Why are malicious npm package numbers so high?
Publishing is fast, install scripts run by default, popular packages sit deep in dependency trees, and automated scanners watch npm closely. Mass spam and token farming campaigns add tens of thousands of packages in a single month.
What was the chalk and debug npm attack?
On 8 September 2025, a maintainer's npm account was compromised and malicious versions of chalk, debug and related packages were published. The payload hijacked cryptocurrency wallet transactions in browsers. The maintainers say the versions were live for about two hours before npm removed them.
What is the Shai-Hulud worm?
A self replicating npm malware campaign first reported in September 2025. It stole developer credentials, published them to public GitHub repositories, and used stolen npm tokens to publish infected versions of the victim's other packages. GitHub removed more than 500 packages in the first wave.
Did PyPI get hit by Shai-Hulud?
No. PyPI says it was not exploited, but it found some long lived PyPI tokens exposed in compromised GitHub repositories and revoked them as a precaution in November 2025.
Does trusted publishing stop supply chain attacks?
It removes long lived publish tokens, which closes the token theft path used by the 2025 worms and CI attacks. It does not stop phishing of interactive accounts, a compromised CI pipeline that publishes legitimately, or new malicious packages.
How long do malicious packages stay on npm or PyPI?
Takeovers of popular packages are usually removed within hours: about two hours for chalk and between two and twelve hours for Nx versions. PyPI handled 92% of malware reports within 24 hours in 2025. Low traffic typosquats can last months; two malicious crates sat on crates.io for about 122 days.
What is a dependency cooldown?
A rule that your tooling will not install a package version until it has been public for a set time, such as three days. Because takeovers of popular packages are usually caught within hours, a short cooldown keeps those versions out of your builds.
How do I check if I installed a compromised npm package?
Search your lockfiles and installed trees with npm ls <package> --all, search artifact caches, and query your SBOMs for the exact versions named in the advisory. Then rotate any credential that was available to the build or developer machine.
How we measured
- OSV counts. We downloaded the OpenSSF malicious packages stats file (
https://ossf.github.io/malicious-packages/stats/all.csv) on 23 October 2026; its stats page showed a last update of 23 October 2026 at 00:55 UTC. The file has one row per ecosystem per month with a count of published reports. We summed rows by calendar year, excluded 40 reports carrying a placeholder month of0001-01-01, and kept the OSV ecosystem labels as published, including two separate VS Code labels. Shares are percentages of each year's all ecosystem total. 2026 is partial, 1 January to 23 October. - Spike removal. For "without spikes" figures we removed the two largest months for the named ecosystem: August and November 2025 for npm, July and September 2026 for RubyGems. For 2026 npm, the two largest months were August (4,078) and July (2,056). We did not remove any record by content.
- Incident windows. Live windows come from the maintainer or registry record only. Where a record gave times in a local zone, we converted to UTC. Where only a duration was stated, we quote it.
- Vendor figures. Counts from security vendors are cited without naming the vendor and labelled as using proprietary detection. We did not combine vendor and OSV numbers in any calculation.
- Disputed figures. Where sources disagree (chalk download footprint, Shai-Hulud second wave size, 2026 worm wave size, PyPI and NuGet decline percentages), we give the range and say which source says what.
- What we did not do. We did not count individual OSV records by package, did not deduplicate across versions, and did not attempt download weighted exposure. Those would need the full record set and registry download data, and they are good next steps for your own data team.
Fix it without waiting for upstream
Malicious versions are pulled by registries, but vulnerable versions stay published, and the same remediation habits that move you off a compromised release move you off a vulnerable one. Most of the work after an incident is getting every project onto a known good version and keeping it there.
In incidents like this one, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.
3. Gold Registry: start from components that are already fixed
registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | The Gold scan GitHub Action in CI, plus Safeguard policies and gates |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch (early access) |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
See how Safeguard would help
You can download the Safeguard apps at safeguard.sh/download and read the CLI documentation at docs.safeguard.sh/docs/cli. The demos below show real flows. None of them detects malicious packages; they show how you move a project to a known good version and check what you run.
This demo is a real CLI session. You install the CLI, open the interactive terminal and ask it to fix the lodash CVE in a repository; it reads the finding and advisory, asks before each command, edits package.json to lodash 4.17.21, runs npm install and npm test with your approval, prints a summary and pushes nothing. The same flow applies when an advisory names a compromised version of a package you depend on: you ask it to move to the clean release and review every step.
Click through it at your own pace, or open it full screen.
This demo sets up the Safeguard runner on your own machine, approves it with its device code, and then scans from the terminal. During a supply chain incident, running scans locally on the machines that build your software is how you produce the SBOMs you will search for an affected version.
Click through it at your own pace, or open it full screen.
This demo connects Claude to the Safeguard MCP server with OAuth and lets you choose which tools Claude can use. Once connected, an incident question such as which projects carry a named package version can be asked in plain language against your own data, with the tool access you approved.
Click through it at your own pace, or open it full screen.
Quick check
In the OSV dataset, roughly what share of npm's 2025 malicious package reports came from just two months?
About 92.7%. August 2025 had 35,099 npm reports and November 2025 had 142,163, out of 191,307 for the year. That is why medians and spike removal matter when comparing ecosystems.
Why would a three day release cooldown have kept the chalk and Nx malicious versions out of most builds?
Because those versions were removed within hours: about two hours for chalk and between roughly two and twelve hours for the Nx packages, according to the maintainers and the Nx advisory. A cooldown does not help against an old typosquat you install on purpose.
Sources
- OpenSSF: malicious-packages repository
- OpenSSF: malicious-packages stats
- OpenSSF: malicious-packages stats file, all.csv
- OSV: MAL-2026-11773
- chalk: Version 5.6.1 published to npm is compromised, issue 656
- Vercel: Critical npm supply chain attack response, 8 September 2025
- GitHub blog: Our plan for a more secure npm supply chain
- GitHub changelog: npm classic tokens revoked, session based auth and CLI token management now available
- CISA: Widespread Supply Chain Compromise Impacting npm Ecosystem
- Nx: Malicious versions of Nx and some supporting plugins were published, GHSA-cxm3-wv7p-598c
- PyPI: PyPI in 2025, a year in review
- PyPI: 2025 blog archive
- PyPI: Incident report, phishing attack
- PyPI: Token exfiltration campaign via GitHub Actions workflows
- PyPI: PyPI and Shai-Hulud, staying secure amid emerging threats
- Rust blog: crates.io malicious crates faster_log and async_println
- RubyGems: Security response
- .NET blog: Strengthening NuGet supply chain security, reducing API key lifetime
- Eclipse Foundation: Open VSX security update, October 2025
- One software supply chain vendor's 2026 open source malware report (fetched 23 October 2026; vendor not named under our editorial policy)
- The Hacker News: North Korean hackers spread 1,700 malicious packages
- Safeguard: OSV malicious package reports by ecosystem, September to October 2026
- Safeguard: State of npm supply chain attacks
- Safeguard: State of software supply chain security 2026
Last reviewed: 23 October 2026. Malicious package datasets update daily; check the linked sources for the latest counts.