Safeguard
Supply Chain Security•

Open Source Supply Chain Attacks by Ecosystem, 2025 to 2026

OSV logged 192,730 malicious package reports in 2025 and 99% were npm, yet two spike months made up 92% of npm's total. The data by ecosystem, explained.

Safeguard Security Team
42 min read

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)202420252026 to 23 OctShare of 2025Share of 2026 so far
npm8,317191,30713,03199.26%72.49%
PyPI2,6451,2451,5200.65%8.46%
RubyGems7911293,3690.07%18.74%
NuGet73822220.01%0.12%
VSCode, open-vsx.orgnone listed1340.01%0.02%
Gonone listed810under 0.01%0.06%
crates.ionone listed118under 0.01%0.10%
Mavennone listed20under 0.01%0%
VSCode, no registry suffixnone listed22under 0.01%0.01%
Packagistnone listed010%0.01%
Gitnone listed10under 0.01%0%
All ecosystems12,491192,73017,977100%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

Measure2025 figureWho countsWhat it includesWhat it misses
OSV published reports, all ecosystems192,730OpenSSF dataset, many contributorsReports with a MAL- record, deduplicated by packagePackages removed before anyone reported them; anything not contributed
Vendor new malicious packagesmore than 454,600One supply chain vendor, proprietary detectionSpam, token farming, potentially unwanted apps, malwareIndependent verification; method not public
PyPI malware reports processedmore than 2,000PyPI adminsReports PyPI acted onUploads blocked or removed without a report; other registries
Shai-Hulud packages removed by npmmore than 500GitHub, 22 Sep 2025Compromised packages removed in response to one wormLater 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)EcosystemIncidentTechniquePrimary record
20 to 28 Jul 2025RubyGemsGems 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 AugMalicious new gemsRubyGems blog, 25 Aug 2025
26 to 28 Jul 2025 (EDT)PyPIPhishing via lookalike domain pypj.org; 4 accounts phished; num2words 0.5.15 and 0.5.16 carried malware and were removedAccount takeover by phishingPyPI incident report, 31 Jul 2025
26 to 27 Aug 2025npmNx packages published with a credential stealing postinstallStolen publish token via CI workflow injectionNx advisory GHSA-cxm3-wv7p-598c
5 to 15 Sep 2025PyPIWorkflows modified to send PyPI tokens stored as GitHub secrets to external servers; affected tokens invalidated; no malicious packages publishedCI secret theftPyPI blog, 16 Sep 2025
8 Sep 2025npmchalk, debug and related packages published with a wallet hijacking payload; live about two hoursMaintainer account takeoverchalk issue 656; Vercel post
14 to 23 Sep 2025npmShai-Hulud worm; more than 500 packages removedToken theft and self propagationGitHub blog 22 Sep; CISA alert 23 Sep
24 Sep 2025crates.iofaster_log and async_println deleted after about 122 days liveTyposquat with runtime payloadRust blog, 24 Sep 2025
Oct 2025, closed 21 OctOpen VSXLeaked publishing tokens used to publish malicious extensions; tokens revokedLeaked token abuseEclipse Foundation, 27 Oct 2025
21 to 24 Nov 2025npmShai-Hulud second wave; PyPI revoked exposed tokens as a precautionToken theft, preinstall execution (reported)PyPI blog, 26 Nov 2025; counts secondary
9 Dec 2025npmAll classic tokens revokedRegistry controlGitHub changelog, 9 Dec 2025
Since Jan 2025, reported 8 Apr 2026npm, PyPI, Go, Rust, PackagistMore than 1,700 packages linked to a North Korea attributed campaignFake developer tooling loadersResearcher findings via secondary coverage
4 Aug 2026npmWorm wave across a caching package familyMaintainer 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

RegistryTrusted publishingToken limits2FA positionSource
npmYes, CI providers including GitHub Actions and GitLabClassic tokens revoked 9 Dec 2025; write tokens up to 90 days; two hour login sessionsOn by default for new packages; FIDO preferredGitHub changelog, 9 Dec 2025; GitHub blog, 22 Sep 2025
PyPIYes; more than 50,000 projectsTrusted publishing tokens are short lived and scopedMore than 52% of active users on non-phishable 2FAPyPI 2025 year in review
NuGetYes, since Sep 2025New keys up to 30 days from 17 Aug 2026; older keys expire 1 Nov 2026Not covered in the sources we fetched.NET blog, 3 Aug 2026
Open VSXNot coveredShorter default token validity; faster revocationNot coveredEclipse Foundation, 27 Oct 2025
RubyGemsAnnounced in 2023 per the RubyGems blog indexNot coveredNot coveredRubyGems blog
crates.ioNot covered in the sources we fetchedNot coveredNot coveredRust 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.

EcosystemDominant by volumeHighest impact documented, 2025 to 2026Execution point
npmSpam and token farming packages; typosquatsMaintainer account takeover (chalk, debug, Nx, Shai-Hulud, 2026 worm waves)preinstall and postinstall scripts; browser bundles
PyPINew malicious projects, typosquatsPhishing account takeover (num2words); CI token theft attemptsetup.py on source installs; import time code
RubyGemsBulk junk and malicious gems from new accountsCredential stealing gems (Jul 2025)Gem install hooks and runtime
crates.ioRare typosquatsWallet key stealers copying real cratesRuntime only; the Rust blog notes nothing ran at build time
GoRare; impersonated module pathsLoader modules in a state linked campaignRuntime
VS Code and Open VSXMalicious extensionsLeaked publisher tokens used to ship malwareExtension activation
NuGet, MavenVery low report countsNo major 2025 to 2026 takeover in our primary sourcesBuild 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.

RankMonthnpm reportsRelative size
1Nov 2025142,163████████████████████████████ (28)
2Aug 202535,099███████ (7)
3Sep 20255,579█
4Oct 20251,414█
5Mar 20251,195█
6Dec 20251,067█
7Jul 20251,030█
8May 2025962█
9Jun 2025802█
10Feb 2025775█
11Jan 2025743█
12Apr 2025478█

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

RankEcosystemReports, 1 Jan to 23 Oct 2026Without the two largest months
1npm13,0316,897
2RubyGems3,369218
3PyPI1,5201,086
4NuGet22not meaningful
5crates.io18not meaningful
6Go10not meaningful
7Open VSX4not 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

  1. Use the median month, not the annual sum, when comparing ecosystems or years. It is resistant to single campaign spikes.
  2. 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.
  3. 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.
  4. 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.
  5. 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

IncidentLive windowHow the window is measuredSource
chalk and related packages, 8 Sep 2025About 2 hoursMaintainer statementchalk issue 656
nx 21.5.0About 4 hours 12 minutesPublish 22:32 UTC 26 Aug to removal 02:44 UTC 27 AugNx advisory
nx 20.12.0About 2 hours 7 minutesPublish 00:37 to removal 02:44 UTC 27 AugNx advisory
@nx/key, @nx/enterprise-cloud 3.2.0About 11 hours 48 minutesPublish 22:32 UTC 26 Aug to removal 10:20 UTC 27 AugNx advisory
PyPI phishing domain pypj.orgAt least about 47 hoursFirst user report to registrar holdPyPI incident report
RubyGems credential stealers, Jul 2025About 3 to 8 daysFlagged 20 Jul to removal 23 to 28 JulRubyGems blog
PyPI malware reports, 202566% within 4 hours; 92% within 24 hoursReport to actionPyPI year in review
faster_log, async_printlnAbout 122 daysPublish 25 May to deletion 24 Sep 2025Rust 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.

ControlStops or limitsDoes not help withEffort
Commit lockfiles and install with npm ci, pip install --require-hashes, cargo build --lockedSilent upgrades to a malicious version during its live windowA malicious version you deliberately addLow
Release cooldown (minimum release age)Hour scale takeover windowsOld typosquatsLow
Disable install scripts by default, allow per packagenpm and Ruby install time payloads, worm propagationRuntime loadersMedium
Trusted publishing for your own packagesToken theft leading to malicious publishes under your namePhished maintainers using interactive loginMedium
Phishing resistant MFA on registry and Git accountsLookalike domain phishingStolen CI tokensLow
CI workflow hardening (no untrusted input in run:, avoid pull_request_target with secrets)Nx style token theftCompromise of a dependencyMedium
Scoped registries and source mappingDependency confusionPublic package takeoversMedium
Queryable SBOM inventoryAnswering "were we exposed to version X" in minutesPreventionMedium
Policy gates on known bad versionsReintroduction after an advisory existsThe first hours before an advisoryLow once SBOMs exist

Step by step: lock down an npm project

  1. Commit the lockfile and install from it in CI.
npm ci
  1. 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>
  1. Check a specific package and version across the tree after an advisory.
npm ls chalk --all
npm ls debug --all
  1. Verify registry signatures and provenance attestations for what you installed.
npm audit signatures
  1. 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.

  1. Pin to a date during an active incident. CISA advised pinning to releases before 16 September 2025 during Shai-Hulud. npm's --before flag 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 of 0001-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 situationWhat to use
A fixed version existsAutonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version
You need to know today whether you are affectedGold Open Source lookup and "Am I affected?" checker, free and without login
You want vulnerable versions to stop shippingThe Gold scan GitHub Action in CI, plus Safeguard policies and gates
Upstream has no fix yet, or will not ship oneA custom Gold request; fork and patch (early access)
You want to start clean on new projectsPull 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

Last reviewed: 23 October 2026. Malicious package datasets update daily; check the linked sources for the latest counts.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.