Between 1 September and 16 October 2026, the OSV database published 2,128 malicious package reports. These are the records whose IDs start with MAL-, fed by the OpenSSF malicious-packages project. npm accounts for 1,299 of them and RubyGems for 677. PyPI has 142.
We counted them from public bulk files downloaded on 18 October 2026. The method is below, so you can rerun it and get the same numbers for the same window.
How we counted
OSV publishes one zip per ecosystem in a public Google Cloud Storage bucket. Each zip holds one JSON file per record. We downloaded the zips for npm, PyPI, RubyGems, NuGet, Go, crates.io, Maven, Packagist, Pub, Hex, GitHub Actions, VSCode, Hackage and CRAN (the curl loop below omits GitHub Actions, which had no MAL- entries), then kept records whose published date falls from 2026-09-01 to 2026-10-16 inclusive (UTC).
for e in npm PyPI RubyGems NuGet Go crates.io Maven Packagist Pub Hex VSCode Hackage CRAN; do
curl -sS -o "$e.zip" "https://osv-vulnerabilities.storage.googleapis.com/$e/all.zip"
done
import zipfile, json, collections
mal = []
for eco in ["npm", "PyPI", "RubyGems", "NuGet", "Go", "crates.io", "Maven",
"Packagist", "Pub", "Hex", "VSCode", "Hackage", "CRAN"]:
z = zipfile.ZipFile(f"{eco}.zip")
for name in z.namelist():
if not name.startswith("MAL-"):
continue
d = json.loads(z.read(name))
if "2026-09-01" <= d["published"][:10] <= "2026-10-16":
d["_eco"] = eco
mal.append(d)
print(len(mal), "reports,", len({d["id"] for d in mal}), "unique IDs")
print(collections.Counter(d["_eco"] for d in mal).most_common())
We counted by the zip an entry came from. All 2,128 IDs are unique, and none is marked withdrawn. For the comparison column we counted GitHub advisory (GHSA-) records in the same zips and window, excluding withdrawn ones.
What's affected
| Ecosystem | Malicious reports | Share | GHSA vulnerability advisories, same window |
|---|---|---|---|
| npm | 1,299 | 61.0% | 533 |
| RubyGems | 677 | 31.8% | 11 |
| PyPI | 142 | 6.7% | 359 |
| NuGet | 5 | 0.2% | 75 |
| Go | 2 | 0.1% | 313 |
| crates.io | 2 | 0.1% | 45 |
| VSCode | 1 | under 0.1% | 0 |
| Total | 2,128 |
Maven, Packagist, Pub, Hex, GitHub Actions, Hackage and CRAN had no MAL- reports in the window.
Two things stand out. On npm there were more malware reports than vulnerability advisories, by about 2.4 to 1. On RubyGems the gap is far larger, 677 to 11, which is a different kind of risk from the one a CVE feed shows you.
One afternoon moved the totals
The weekly counts are not smooth. Weeks start on Monday, so the first and last rows are partial.
| Week starting | Reports |
|---|---|
| 2026-08-31 (from 1 Sep) | 229 |
| 2026-09-07 | 140 |
| 2026-09-14 | 150 |
| 2026-09-21 | 899 |
| 2026-09-28 | 287 |
| 2026-10-05 | 302 |
| 2026-10-12 (to 16 Oct) | 121 |
Of the 899 in the week of 21 September, 715 were published on 22 September alone. 611 of those were RubyGems entries, published between 16:05 and 23:20 UTC, and 608 of the 611 cite only the reversing-labs source. Without that batch, RubyGems would have 66 reports in the window. Every one of the 46 days in the window had at least one report.
Who reported them
Each record can list one or more origins in database_specific.malicious-packages-origins. Counting records that cite each source:
| Source | Records citing it |
|---|---|
| amazon-inspector | 1,054 |
| reversing-labs | 672 |
| ghsa-malware | 599 |
| kam193 | 130 |
| ossf-package-analysis | 107 |
| no origin listed | 77 |
These add up to more than 2,128 because 497 records cite more than one source. All 599 ghsa-malware records are npm, and each carries a GHSA- alias. The 77 records with no origin are mostly npm (68).
What the records contain
263 records carry indicators of compromise in database_specific.iocs: 144 npm, 72 PyPI, 42 RubyGems, 2 Go, 2 crates.io and 1 VSCode. The median record lists one affected version, and 239 records list no specific version. 474 of the 1,299 npm reports (36.5%) are scoped packages whose names start with @.
How to check if you're exposed
OSV's API can check a package name against these records. This script reads an npm package-lock.json (lockfile v2 or later) and prints any package that has a MAL- ID. We ran it against a sample lockfile.
import json, sys, urllib.request
lock = json.load(open(sys.argv[1]))
names = sorted({p.split("node_modules/")[-1] for p in lock["packages"] if p})
body = {"queries": [{"package": {"name": n, "ecosystem": "npm"}} for n in names]}
req = urllib.request.Request("https://api.osv.dev/v1/querybatch", json.dumps(body).encode())
res = json.load(urllib.request.urlopen(req))["results"]
for n, r in zip(names, res):
mal = [v["id"] for v in r.get("vulns", []) if v["id"].startswith("MAL-")]
if mal:
print(n, mal)
print(len(names), "packages checked")
For a single package, curl -s -X POST https://api.osv.dev/v1/query -d '{"package":{"name":"NAME","ecosystem":"npm"}}' returns every record, malware included. Note that the query matches by name, so a hit means "a malicious version exists", and you still need to compare versions.
What to do now
- Run the check on every lockfile you ship from, including
Gemfile.lockandrequirements.txtequivalents, using the matching ecosystem name in the query. - Treat any
MAL-hit as an incident until proven otherwise. Read the record'sdetails, compare the listed versions with your lockfile, and look at theiocsfield for domains and file hashes to search for in logs. - Rotate secrets on any machine that installed a hit. Install-time scripts run with the developer's or CI runner's environment.
- Use a cooldown for new versions. Waiting a few days before adopting a brand new release gives registries and scanners time to flag a bad one.
- Alert on MAL IDs separately from CVEs. A CVE feed will miss everything in the table above.
Why it matters
Vulnerability counts are the number most programs track, and in this window they were the smaller problem for npm and RubyGems. A package that is malicious from its first release has no fixed version and no CVSS score, so a scanner that only reads CVEs will not flag it.
The data also shows how lumpy detection is. One source added 608 RubyGems reports in an afternoon. Counts like these tell you what was reported, not how many malicious packages were published, so read them as a floor on that second number.
Try it yourself
Pick one lockfile from your busiest repo and run the script above. Then look up a package you added in the last month in the Gold directory at https://gold.safeguard.sh, which lists malicious packages alongside CVEs. The demo below walks through spotting typosquats and malware.
Click through it at your own pace, or open it full screen.
Quick check
How many MAL reports did OSV publish from 1 Sep to 16 Oct 2026?
2,128, all with unique IDs and none withdrawn: 1,299 npm, 677 RubyGems, 142 PyPI, 5 NuGet, 2 Go, 2 crates.io and 1 VSCode.
Why do the source counts add up to more than 2,128?
A record can cite more than one origin. 497 of the 2,128 records cite two or more sources.
Why does a CVE scanner miss most of these packages?
Malicious packages are reported under MAL- IDs, with no CVE, no fixed version and no CVSS score, so tools that only read CVE data never see them.