← Back to Feed CACHED · 2026-05-17 09:42:19 · cache_key CVE-2025-29912
CVE-2026-45321 · CWE-506 · Disclosed 2026-05-12

On 2026-05-11

ASSESSED — NOISGATE V0.5
Vendor
Reassessed
Verdict:
01 · The Real Story

This was a poisoned fuel truck, not a hole in every parked car

CVE-2026-45321 covers a real npm supply-chain compromise, not a theoretical bug: between 2026-05-11 19:20 and 19:26 UTC, attackers published **84 malicious versions across 42 @tanstack/* packages** using TanStack's legitimate GitHub Actions OIDC trusted-publisher flow. The payload executed at install time through a malicious optionalDependencies chain, dropped an obfuscated router_init.js, stole cloud, GitHub, npm, Vault, Kubernetes, and SSH credentials, and attempted self-propagation by republishing packages the victim maintained. Affected versions were package-specific; clean follow-up versions were issued per package, and TanStack states all currently available package versions are now safe to install.

The vendor's CRITICAL 9.6 is technically defensible for impact, but it overstates *current enterprise patch priority* because this is not an always-reachable server-side exploit. Real-world exposure requires an install event against the exact bad versions, usually during the short publication window or via stale lockfiles/caches. That friction pulls the score down one bucket. KEV status and confirmed in-the-wild abuse keep it firmly HIGH anyway, because if your CI or developer host touched those versions, the blast radius is credential theft and downstream package compromise.

"Brutal if you pulled the bad versions, but this is not a standing internet-wide exploit anymore."
02 · The Attack Path

4 steps from start to impact.

STEP 01

Compromise the maintainer pipeline

The attacker abused a pull_request_target workflow pattern, cross-fork GitHub Actions cache poisoning, and runtime extraction of an OIDC token from the runner process to gain publish capability without stealing npm tokens. This is a repo-specific maintainer compromise step, not something an attacker can replay against your estate unless they are targeting the upstream project itself. Weaponization details and IOCs are public in the GHSA and incident issue.
Conditions required:
  • Attacker can submit or manipulate a fork PR against TanStack/router
  • The upstream workflow still uses the vulnerable trust boundary pattern
  • GitHub Actions cache and id-token: write are reachable in the release path
Where this breaks in practice:
  • This path was specific to TanStack's release workflow and has been remediated upstream
  • Most enterprises are consumers here, not the direct target of this initial step
Detection/coverage: Traditional vuln scanners will not see this precursor. CI security tools, workflow review, and GitHub Actions hardening are the right controls.
STEP 02

Publish trusted-but-malicious package versions

Using the stolen OIDC publish capability, the attacker pushed two malicious versions for each affected package during a roughly six-minute window on 2026-05-11. Because publishes came through trusted identity, provenance and normal trust signals looked legitimate. npm later deprecated and removed the malicious tarballs, sharply reducing forward exposure.
Conditions required:
  • Attacker already controls upstream release execution
  • Victim dependency tooling can resolve the published bad versions
Where this breaks in practice:
  • The bad versions were short-lived: deprecated within about 1 hour 43 minutes and removed by npm within about 4 hours 35 minutes
  • Current upstream package versions are clean according to TanStack
Detection/coverage: SCA tools and registry intelligence can now match the exact bad versions. During the incident window, only fast registry telemetry vendors were likely to catch it.
STEP 03

Land on a developer or CI host during install

Impact requires npm install, pnpm install, or yarn install to resolve one of the compromised versions, often from a lockfile, cache, or a fresh dependency resolution during the incident window. The malicious manifest referenced a fake @tanstack/setup optional dependency pointing to a GitHub commit, which then executed a prepare script and launched router_init.js. This is the decisive friction point: no install, no compromise.
Conditions required:
  • The environment depends directly or transitively on one of the 42 affected packages
  • A build or workstation performed an install against a malicious version
  • Lifecycle scripts were allowed to run
Where this breaks in practice:
  • Enterprises that did not install during the window, or that never pinned the bad versions, were never exposed
  • ignore-scripts, strict artifact proxying, frozen lockfiles, and package cooldown policies break this step
  • A lot of production servers never execute npm installs at all
Detection/coverage: SCA is decent post-disclosure. Endpoint detections may catch Bun execution, router_init.js, or access to Session/Oxen domains, but only on the hosts that actually performed installs.
STEP 04

Steal secrets and spread

Once executed, the payload harvested AWS/GCP/Kubernetes/Vault credentials, GitHub tokens, npm tokens, and SSH keys, then exfiltrated via the Session/Oxen network and enumerated packages maintained by the victim for republishing. On a developer box or CI runner with broad cloud access, this becomes an identity and software supply-chain incident, not a simple dependency bug.
Conditions required:
  • The install host has valuable credentials or metadata access
  • Outbound access to the exfiltration path is available
  • Stolen credentials are still valid
Where this breaks in practice:
  • Ephemeral runners, scoped secrets, blocked metadata access, and egress controls limit blast radius
  • Hosts with no cloud creds or maintainer privileges are much less interesting
Detection/coverage: Look for access to filev2.getsession.org, seed1.getsession.org, seed2.getsession.org, seed3.getsession.org, the fake @tanstack/setup specifier, and router_init.js indicators. EDR/forensics are required; patch scanners alone are not enough.
03 · Intelligence Metadata

The supporting signals.

In-the-wild statusYes. This CVE documents an actual malicious publish event, not lab-only research. TanStack's postmortem and the GHSA confirm live exploitation on 2026-05-11.
KEV statusKEV listed: YES per the supplied intel. CISA's KEV catalog is the authoritative reference, but the exact add date was not independently verified during this session.
EPSS0.17051 from the supplied intel. Percentile was not independently verified during this session.
Proof-of-concept / weaponizationPublic weaponization details are already available in issue #7383 and the GHSA, including the exact optionalDependencies IOC and non-executing npm pack verification commands. This is effectively beyond PoC: the malicious artifacts were live.
CVSS meaningCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H says the attacker needed no privileges and could reach victims over the network, but user interaction is required: the victim must install a bad version. The score captures impact well, but not the short-lived publication window.
Affected footprintExactly **42 @tanstack/* packages, 84 malicious versions, two bad versions per package, published between 19:20 and 19:26 UTC on 2026-05-11**.
Fixed versionsThere is no single patched version; each package has its own clean follow-up release, e.g. @tanstack/react-router 1.169.9, @tanstack/router-core 1.169.9, @tanstack/history 1.161.13. TanStack says every currently available published version is now safe.
Exposure realityThis is not internet-exposed service attack surface. Reachability depends on developer or CI installs. That said, affected packages were widely consumed; Socket highlighted @tanstack/react-router at roughly 12M weekly downloads.
Detection fingerprintThe strongest IOC is the fake dependency specifier `@tanstack/setup -> github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c plus the presence of router_init.js`.
Disclosure / reporterPublic detection was reported by ashishkurmi of StepSecurity on 2026-05-11 via TanStack/router issue #7383; the GitHub advisory was published 2026-05-11/12 depending on source timezone rendering.
04 · The Call

noisgate verdict.

Final Verdict
DOWNGRADED to HIGH (8.3/10)

The single biggest downward pressure is that this requires an install event against specific short-lived malicious package versions, not generic reachability to a perpetually vulnerable service. KEV status and real credential-theft impact keep it high, but the exposed population is dramatically smaller than the vendor CVSS suggests once you account for the narrow time window and stale-lockfile dependence.

HIGH Incident timeline, affected versions, and technical mechanism
MEDIUM How much of a given enterprise actually installed the bad versions

Why this verdict

  • Downgrade for exposure friction: the malicious versions were live only for hours, not months, and TanStack says current published versions are safe.
  • Downgrade for attacker path realism: your hosts are only affected if they ran an install that resolved the exact bad versions, typically during the publication window or from stale lockfiles/caches.
  • Hold at HIGH because impact is ugly: if a developer workstation or CI runner did pull these versions, this becomes credential theft, cloud access abuse, and potential downstream package republishing.
  • KEV is an upward force: confirmed exploitation in the wild means this is not hypothetical and warrants immediate triage even after the downgrade.

Why not higher?

I am not calling this CRITICAL because it is not a standing unauthenticated remote compromise path into broad enterprise populations. Most endpoints and servers were never reachable unless they performed package installation against those exact versions, and the malicious artifacts were deprecated and removed quickly.

Why not lower?

I am not dropping this to MEDIUM because successful hits are far more than nuisance-level dependency risk. The payload targeted high-value developer and CI secrets, supported self-propagation, and the user-supplied KEV status says attackers were already exploiting it in the wild.

05 · Compensating Control

What to do — in priority order.

  1. Block the bad versions in your registry path — Use your private npm proxy, artifact manager, or egress controls to deny the exact malicious version set and the IOC git ref immediately, within hours because KEV/active exploitation overrides the normal HIGH timetable. This prevents stale lockfiles, cached builds, and air-gapped mirrors from reintroducing the package even though upstream npm removed it.
  2. Hunt for stale lockfiles and caches — Search source repos, CI workspaces, build images, and package caches for the 42 package/version combinations, the fake @tanstack/setup dependency, and router_init.js immediately, within hours. This is the real control that separates 'noise in threat intel' from 'we actually executed the implant.'
  3. Treat any hit as credential compromise — If a host installed an affected version, rotate GitHub, npm, SSH, cloud, Kubernetes, and Vault credentials reachable from that host immediately, within hours. The malware's whole value proposition was identity theft, so version cleanup without secret rotation is incomplete containment.
  4. Rebuild polluted build and dev environments — Reimage or rebuild affected CI runners, developer workstations, and golden images from known-clean baselines immediately, within hours for confirmed hits. This matters because the risk is host compromise and persistence, not just a bad dependency entry.
  5. Temporarily suppress lifecycle-script installs where feasible — For emergency containment, enable ignore-scripts or equivalent on high-risk pipelines that do not need install-time scripts, and deploy the control within hours while the hunt runs. It is not a permanent fix for every Node workflow, but it cuts off this attack class fast.
What doesn't work
  • Just upgrading to a clean TanStack version does not clean a host that already executed the malicious install; the real problem is stolen credentials and possible persistence.
  • Trusted Publishing, Sigstore, or SLSA provenance badges do not save you here; the attacker published through a legitimate trusted identity.
  • Rotating npm tokens alone is insufficient because TanStack's postmortem says npm tokens were not the path used; the publish abuse came from GitHub Actions OIDC.
06 · Verification

Crowdsourced verification payload.

Run this on the target developer workstation, CI runner, build image mount, or source checkout root you want to assess. Invoke it as python3 tanstack_cve_2026_45321_check.py /path/to/repo-or-host-snapshot; it needs only read access to lockfiles, caches, and node_modules, not admin privileges.

noisgate-verify.py
PYTHONREAD-ONLYSAFE
#!/usr/bin/env python3
# CVE-2026-45321 TanStack verifier
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN

import json
import os
import re
import sys
from pathlib import Path

BAD = {
    "@tanstack/arktype-adapter": {"1.166.12", "1.166.15"},
    "@tanstack/eslint-plugin-router": {"1.161.9", "1.161.12"},
    "@tanstack/eslint-plugin-start": {"0.0.4", "0.0.7"},
    "@tanstack/history": {"1.161.9", "1.161.12"},
    "@tanstack/nitro-v2-vite-plugin": {"1.154.12", "1.154.15"},
    "@tanstack/react-router": {"1.169.5", "1.169.8"},
    "@tanstack/react-router-devtools": {"1.166.16", "1.166.19"},
    "@tanstack/react-router-ssr-query": {"1.166.15", "1.166.18"},
    "@tanstack/react-start": {"1.167.68", "1.167.71"},
    "@tanstack/react-start-client": {"1.166.51", "1.166.54"},
    "@tanstack/react-start-rsc": {"0.0.47", "0.0.50"},
    "@tanstack/react-start-server": {"1.166.55", "1.166.58"},
    "@tanstack/router-cli": {"1.166.46", "1.166.49"},
    "@tanstack/router-core": {"1.169.5", "1.169.8"},
    "@tanstack/router-devtools": {"1.166.16", "1.166.19"},
    "@tanstack/router-devtools-core": {"1.167.6", "1.167.9"},
    "@tanstack/router-generator": {"1.166.45", "1.166.48"},
    "@tanstack/router-plugin": {"1.167.38", "1.167.41"},
    "@tanstack/router-ssr-query-core": {"1.168.3", "1.168.6"},
    "@tanstack/router-utils": {"1.161.11", "1.161.14"},
    "@tanstack/router-vite-plugin": {"1.166.53", "1.166.56"},
    "@tanstack/solid-router": {"1.169.5", "1.169.8"},
    "@tanstack/solid-router-devtools": {"1.166.16", "1.166.19"},
    "@tanstack/solid-router-ssr-query": {"1.166.15", "1.166.18"},
    "@tanstack/solid-start": {"1.167.65", "1.167.68"},
    "@tanstack/solid-start-client": {"1.166.50", "1.166.53"},
    "@tanstack/solid-start-server": {"1.166.54", "1.166.57"},
    "@tanstack/start-client-core": {"1.168.5", "1.168.8"},
    "@tanstack/start-fn-stubs": {"1.161.9", "1.161.12"},
    "@tanstack/start-plugin-core": {"1.169.23", "1.169.26"},
    "@tanstack/start-server-core": {"1.167.33", "1.167.36"},
    "@tanstack/start-static-server-functions": {"1.166.44", "1.166.47"},
    "@tanstack/start-storage-context": {"1.166.38", "1.166.41"},
    "@tanstack/valibot-adapter": {"1.166.12", "1.166.15"},
    "@tanstack/virtual-file-routes": {"1.161.10", "1.161.13"},
    "@tanstack/vue-router": {"1.169.5", "1.169.8"},
    "@tanstack/vue-router-devtools": {"1.166.16", "1.166.19"},
    "@tanstack/vue-router-ssr-query": {"1.166.15", "1.166.18"},
    "@tanstack/vue-start": {"1.167.61", "1.167.64"},
    "@tanstack/vue-start-client": {"1.166.46", "1.166.49"},
    "@tanstack/vue-start-server": {"1.166.50", "1.166.53"},
    "@tanstack/zod-adapter": {"1.166.12", "1.166.15"}
}

IOC_REF = "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c"
IOC_FILE = "router_init.js"
findings = []
files_scanned = 0


def add(kind, path, detail):
    findings.append({"kind": kind, "path": str(path), "detail": detail})


def check_name_version(name, version, path, source):
    if name in BAD and version in BAD[name]:
        add(source, path, f"{name}@{version}")


def walk_json(obj, path, source):
    if isinstance(obj, dict):
        name = obj.get("name")
        version = obj.get("version")
        if isinstance(name, str) and isinstance(version, str):
            check_name_version(name, version, path, source)
        if "optionalDependencies" in obj and isinstance(obj["optionalDependencies"], dict):
            if obj["optionalDependencies"].get("@tanstack/setup") == IOC_REF:
                add(source, path, "malicious optionalDependencies IOC")
        for v in obj.values():
            walk_json(v, path, source)
    elif isinstance(obj, list):
        for v in obj:
            walk_json(v, path, source)


def scan_package_lock(path):
    global files_scanned
    files_scanned += 1
    try:
        data = json.loads(path.read_text(encoding="utf-8", errors="ignore"))
        walk_json(data, path, "lockfile")
        packages = data.get("packages") if isinstance(data, dict) else None
        if isinstance(packages, dict):
            for meta in packages.values():
                if isinstance(meta, dict):
                    name = meta.get("name")
                    version = meta.get("version")
                    if isinstance(name, str) and isinstance(version, str):
                        check_name_version(name, version, path, "lockfile")
    except Exception as e:
        add("error", path, f"package-lock parse failed: {e}")


def scan_text_lock(path):
    global files_scanned
    files_scanned += 1
    try:
        txt = path.read_text(encoding="utf-8", errors="ignore")
        if IOC_REF in txt:
            add("lockfile", path, "malicious git ref IOC")
        for pkg, versions in BAD.items():
            for ver in versions:
                patterns = [
                    re.escape(f"{pkg}@{ver}"),
                    re.escape(f"{pkg}@npm:{ver}"),
                    re.escape(f"/{pkg}@{ver}"),
                    re.escape(f"'{pkg}@{ver}'"),
                    re.escape(f'"{pkg}@{ver}"')
                ]
                if any(re.search(p, txt) for p in patterns):
                    add("lockfile", path, f"{pkg}@{ver}")
    except Exception as e:
        add("error", path, f"text lock parse failed: {e}")


def scan_package_json(path):
    global files_scanned
    files_scanned += 1
    try:
        data = json.loads(path.read_text(encoding="utf-8", errors="ignore"))
        name = data.get("name")
        version = data.get("version")
        if isinstance(name, str) and isinstance(version, str):
            check_name_version(name, version, path, "manifest")
        opt = data.get("optionalDependencies")
        if isinstance(opt, dict) and opt.get("@tanstack/setup") == IOC_REF:
            add("manifest", path, "malicious optionalDependencies IOC")
    except Exception as e:
        add("error", path, f"package.json parse failed: {e}")


def scan_router_init(path):
    global files_scanned
    files_scanned += 1
    try:
        if path.name == IOC_FILE and path.stat().st_size > 1024 * 1024:
            add("file", path, "router_init.js present and >1MB")
    except Exception as e:
        add("error", path, f"router_init stat failed: {e}")


def main():
    root = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd()
    if not root.exists():
        print("UNKNOWN - path does not exist")
        sys.exit(2)

    for p in root.rglob("*"):
        if not p.is_file():
            continue
        name = p.name
        if name in {"package-lock.json", "npm-shrinkwrap.json"}:
            scan_package_lock(p)
        elif name in {"pnpm-lock.yaml", "yarn.lock"}:
            scan_text_lock(p)
        elif name == "package.json":
            scan_package_json(p)
        elif name == IOC_FILE:
            scan_router_init(p)

    real_findings = [f for f in findings if f["kind"] != "error"]
    errors = [f for f in findings if f["kind"] == "error"]

    if real_findings:
        print("VULNERABLE")
        for f in real_findings[:100]:
            print(f"- {f['kind']}: {f['path']} :: {f['detail']}")
        if errors:
            print(f"- note: {len(errors)} parse/stat errors encountered")
        sys.exit(1)

    if files_scanned == 0:
        print("UNKNOWN - no relevant Node lockfiles or manifests found")
        sys.exit(2)

    if errors and files_scanned < len(errors):
        print("UNKNOWN - only errors encountered during scan")
        sys.exit(2)

    print("PATCHED")
    print(f"Scanned {files_scanned} relevant files under {root}")
    if errors:
        print(f"Note: {len(errors)} non-fatal parse/stat errors encountered")
    sys.exit(0)


if __name__ == "__main__":
    main()
07 · Bottom Line

If you remember one thing.

TL;DR
Monday morning, do not treat this like routine dependency hygiene. First, use the noisgate mitigation SLA override for KEV/active exploitation and patch / mitigate immediately, within hours: block the bad versions and IOC git ref in your package path, hunt every repo/build cache for stale lockfiles, and escalate any confirmed install as an endpoint-and-identity incident with secret rotation and host rebuild. Then use the noisgate remediation SLA for a HIGH finding—≤180 days—to finish normalizing all repos, images, and dependency baselines onto clean TanStack releases, though any environment that actually executed a malicious install should be remediated now, not left for that outer window.

Sources

  1. TanStack GitHub Security Advisory GHSA-g7cv-rxg3-hmpx
  2. TanStack postmortem
  3. NVD CVE record
  4. Incident tracking issue #7383
  5. Socket TanStack incident write-up
  6. GitLab advisory mirror
  7. CISA Known Exploited Vulnerabilities Catalog
Peer Review

What defenders are saying.

Submit a review attribution: handle + country only
0 flags selected · stored anonymously
Validation Results

Crowdsourced verification outputs.

Results submitted by users who ran the verification payload against their environment.