GitHub Shipped an npm Firewall Today. The Attack That Put TanStack on CISA's Must-Patch List Is the One Shape It Cannot See — and Neither Can Ours.
GitHub launched an npm firewall this morning, in public preview for GitHub Enterprise Cloud. It sits between your endpoints or internal registry and the upstream repository, inspects metadata requests and package downloads, and blocks suspicious packages during installation. npm only, for now. GitHub's stated reason is the right one: AI agents and CI runners install dependencies automatically, and automatic installation is how malware gets a shell on a build machine at three in the morning with nobody watching.
This is good. Ship it, turn it on, we are not here to pick at it. We run the same category of control ourselves and have since March, so this is a note from inside the same problem rather than from the stands.
What we want to do is name the residual — the attack shape that survives a package firewall, including ours. It is not hypothetical and it is not obscure. It is sitting in CISA's Known Exploited Vulnerabilities catalog, which means the federal government has confirmed it was used against real people.
[CVE-2026-45321](https://analytics.dugganusa.com/api/v1/dredd/kev-gap?cve=CVE-2026-45321)
CISA's own description, from catalog version 2026.10.28:
TanStack contains an unspecified vulnerability that allowed malicious versions of the product to be published to the npm registry to publish credential-stealing malware under a trusted identity.
Added May 27, 2026. Read that last clause again, because it is the whole post: under a trusted identity.
Nothing was typosquatted. No lookalike name, no suspicious new maintainer, no fresh account with zero history, no unsigned artifact. The malicious versions were published to the real package, by the real publishing pipeline, carrying the real identity. The provenance was valid because the provenance was genuinely produced by the attacker's path into the trusted publishing workflow.
Now ask what any package firewall sees when that version arrives. It inspects the metadata: the package is popular, long-lived, has a reputable maintainer and a healthy download curve. It inspects provenance: valid, signed, attested, linked to the expected repository. It checks the deny list: nothing, because nobody has reported it yet — the whole point of a fresh compromise is that it is fresh. Every input says fine. The verdict is ALLOW, and the verdict is correct given the inputs and completely wrong about the world.
The sentence we make our own tool say
We run a pre-install membrane. It carries a deny list of more than 230,000 named malicious packages built from OSV and our own hunting, plus a reputation heuristic, across npm, PyPI, RubyGems — 4,333 RubyGems entries as of this morning — Packagist and GitHub Actions. It is free, it has no sales call attached, and it is wired into an MCP server so an agent can ask before it installs.
Here is what it returns on a package it has never heard of, verbatim, because we made it print its own limit rather than a green tick:
ALLOW — Not on the DugganUSA malicious-package deny-list, and reputation is unremarkable. Absence is not proof of safety — pin and review new dependencies.
That caveat is not modesty. It is the honest translation of what a deny list can tell you, which is "not reported," not "not malicious." Any control built on reputation, provenance and known-bad lists shares that ceiling, because all three are measurements of the past, and a trusted-identity compromise is an event that has no past. GitHub's firewall inspects metadata and downloads. So do we. Metadata about a compromised-but-legitimate package describes a legitimate package.
We would rather say that plainly than let a customer read ALLOW as SAFE. A control that overstates itself is worse than no control, because it buys the thing security controls are actually bought for, which is the decision to stop looking.
We get this wrong in the other direction too
Honesty runs both ways, so: yesterday our own checker flagged Microsoft's playwright-mcp as a typosquat. It was not. It is the real package from the real vendor, and our matcher had decided a package was the thing it resembled. We fixed it — a typosquat is not the package it squats — and the fix shipped.
That is the other failure mode of this entire product category, and it is the one that actually gets controls turned off. A firewall that blocks Microsoft's own tooling in your CI gets disabled by lunchtime, and then it is not protecting you from anything. Precision is not a nicety here; it is the thing that keeps the control switched on.
We also published a post two days ago about a dependency our check flagged, where we did the work and it turned out to be fine. We think publishing the false positives next to the catches is the only way any of these numbers mean anything.
What actually closes the residual
Nothing on this list is a product. All of it is free and most of it is one line.
Pin exact versions and commit the lockfile. A trusted-identity compromise ships as a new version. A pinned dependency does not take a new version because the registry offered one. This is the single highest-value control on the list, and it predates every firewall.
Put a cooldown on upgrades. Most malicious versions are caught and yanked within hours to days. A policy that refuses any release younger than, say, seventy-two hours converts most of this class of attack into somebody else's incident report. It costs you three days of not having the newest patch, which is almost never the thing that hurts you.
Turn off lifecycle scripts. npm ci --ignore-scripts removes the install-time execution that most of this malware depends on. If a dependency genuinely needs a postinstall script, that is worth knowing about explicitly.
Give the CI runner nothing worth stealing. The credential-stealing payload in the TanStack case wanted environment variables and token files. A build job scoped to a short-lived token with no registry write access and no cloud credentials is a bad day for that payload. This is the control that works even when every detection layer has failed, which is why it is the one we would keep if we could only keep one.
Watch what the build reaches for, not just what it installed. The firewall sits at install time. Egress from a build runner to an address nobody recognized is the signal that survives when install-time inspection returned ALLOW. Our free blocklist is one of the lists you can point at that egress, and you can pull it with a curl: the IP, domain and hash feeds are at analytics.dugganusa.com/api/v1/stix-feed. Registration is free and takes about thirty seconds, and you set the confidence floor, not us.
Why we are glad GitHub shipped it anyway
Because the base rate matters more than the residual. The overwhelming majority of malicious package installs are not sophisticated trusted-identity compromises. They are typosquats, abandoned-package takeovers, and fresh throwaway accounts publishing something with a postinstall script — and a metadata-and-download inspector is genuinely good at all three. A control that catches the common case and misses the rare one is a control worth having. It is only dangerous if someone sells it as the whole answer.
Two things we would like to see next, said as a colleague rather than a competitor. First, other ecosystems — PyPI and RubyGems have the same problem and less tooling, and the shops most exposed are the ones least likely to be on Enterprise Cloud. Second, the tier. This ships to GitHub Enterprise Cloud customers. The developer installing a dependency on a laptop at a two-person shop is the one with no AppSec team, no budget and the same attack surface. That gap is the entire reason our version is free.
Limits
We have not tested GitHub's npm firewall. Everything we say about how it works is from the public preview announcement published today, and if it already does something we describe as missing, that is our error and we will correct it here.
CVE-2026-45321 is read from CISA's catalog, version 2026.10.28, 1,762 entries, pulled today. The quoted description is CISA's, not ours. We did not independently analyze the TanStack compromise and we hold no indicators from it.
Our deny-list figure of 230,000-plus named packages is a count of entries, not a count of distinct attacks, and it is substantially built from OSV and other public sources rather than our own discovery — we aggregate far more of it than we find. The RubyGems figure of 4,333 was measured this morning.
We have no measurement of how often the trusted-identity shape occurs relative to the common cases. We are asserting that it survives this class of control and that it is in KEV. We are not asserting it is frequent, and anyone telling you they know that ratio should be asked how they counted.
Five percent of this is wrong somewhere. It always is.
Was this useful? Rate this post — the widget is at the bottom of the page, and we read every response. If you run a package firewall and have caught something in the trusted-identity shape, we would genuinely like to hear it.
Every indicator in this post is in the feed. Free.
1.58M+ IOCs, STIX 2.1 / TAXII, 88% novel vs ThreatFox, exploited-CVE leads ahead of CISA. No credit card — a free API key in 30 seconds, and you can audit every claim above against the live endpoints.



Comments