TL;DR

tinypool is the minimal worker_threads pool that Vitest spun out of piscina; it moved 59,633,803 copies in the week ending 2026-10-04, and Vitest itself 142,142,023. On 2026-08-30 its maintainers published two advisories, each a prototype-pollution gadget: tinypool does not pollute anything itself, but if any other code in the process has already written to Object.prototype, tinypool turns that into code execution inside every worker it spawns, "with the privileges of the host process."

  • GHSA-5gmw-xhrv-c9v3 / CVE-2026-104848 — ThreadPool.options is built with object spread from a normal options object, which materialises inherited properties as own properties, and execArgv and env are then handed to new Worker(). Node's own protection against inherited worker options is defeated. A polluted Object.prototype.execArgv = ['--require', '/path/attacker.js'] or Object.prototype.env = { NODE_OPTIONS: … } executes in each worker. ≤ 2.1.0 → 2.1.1. CVSS 4.0 9.5. Reporter ambushneupane; PR #134.
  • GHSA-85c8-ppgw-ccpr / CVE-2026-104849 — pool.run(task, options) reads options.filename without requiring an own property, so a polluted Object.prototype.filename "can replace the intended worker module." Only triggers when the caller passes its own options object to run(); inherited from piscina. < 2.1.2 → 2.1.2. CVSS 4.0 9.5. Reporter Fcmam5; PR #135.

The registry shows 2.1.1 at 2026-08-23 14:26 UTC and 2.1.2 at 15:03 UTC; the advisories followed a week later, the CVE records on 2026-10-02, and the database's reviewed npm critical listing on 10-05 — which is how a six-week-old fix looked like a fresh wave. The problem for Vitest users: the affected range is everything below 2.1.1, there is no 1.x or 0.x patch (the version list goes 1.1.1 → 2.0.0), and npm view on 2026-10-06 shows Vitest 3.2.x pinning tinypool: ^1.1.1, 2.1.x ^1.0.1, 1.6.x ^0.8.3; Vitest 4.x and 5.x do not list tinypool as a dependency at all. So npm update fixes a direct tinypool dependency and fixes nothing for a Vitest 1–3 project; the fix there is Vitest 4+.

What happened

Why "gadget" and why high rather than the vendor's critical. Both advisories require a prior prototype pollution — an unrelated bug in a merge, deep-clone, query-string or config library — before tinypool does anything wrong. The vendor scores them 9.5 with AT:P (attack requirements present) to encode exactly that; this repo's severity reflects that an attacker needs two bugs, not one, while the consequence (arbitrary code in a process that may hold CI secrets or a service's credentials) is the full one. If your dependency graph has a known prototype-pollution CVE, treat this as critical; the gadget is what makes a "low-impact" pollution finding exploitable.

The mechanism, in the vendor's words. For CVE-2026-104848: "Tinypool constructs ThreadPool.options from a normal options object and reads the execArgv and env worker options … allowing values inherited from a polluted Object.prototype to reach new Worker()." The root cause is the spread ({...options}), which copies inherited keys onto a fresh object — after which Node's check that worker options are own properties sees own properties. For CVE-2026-104849: "If that object does not have an own filename property, the lookup falls through to Object.prototype." The limitation is real: calls to run() without a second argument use tinypool's trusted default and are not affected.

Where it sits. Vitest's default pool: 'threads' and 'forks' runners used tinypool through 3.x; Vitest's own advisory tab (fetched 2026-10-06) does not carry an entry for either CVE, and the GHSA for CVE-2026-104848 is the source for "tinypool's role in Vitest (~42M weekly downloads)" — the registry figure is now 142M. Tinypool is also used directly by servers that offload CPU-bound work per request, which is the deployment where an attacker-reachable prototype pollution is most likely to exist in the same process.

Am I affected?

npm ls tinypool vitest --all 2>/dev/null | head -20
grep -n '"tinypool"' package-lock.json 2>/dev/null | head
# Which Vitest line pins which tinypool range (2026-10-06 values: 3.x ^1.1.1, 2.x ^1.0.1, 1.x ^0.8.3; 4.x/5.x none)
npm view vitest@3 dependencies.tinypool
# Known prototype-pollution findings elsewhere in the tree — the first half of the chain:
npm audit --json 2>/dev/null | grep -i -c 'prototype pollution'
  • Direct dependency on tinypool < 2.1.2 → affected by both; 2.1.1 → still CVE-2026-104849.
  • Vitest 1.x–3.x → tinypool 0.x/1.x in the tree, no in-range fix. The practical exposure in a test runner is a polluted prototype inside the Vitest main process — a malicious dependency, a test that imports untrusted fixtures, or a vulnerable transitive library — which then runs code in every test worker with the CI job's credentials.
  • Vitest 4.x / 5.x → tinypool is not a listed dependency (checked vitest@4.1.11, vitest@5.0.3, and @vitest/runner for both).

If you are affected

  1. Direct users: upgrade to tinypool@2.2.0 (latest; 2.1.2 is the minimum). Vitest 1–3 users: upgrade Vitest to 4.x or later; there is no tinypool release that satisfies ^1.1.1 and carries the fix, and overriding the transitive version to 2.x is a major bump tinypool's API may not honour — test it rather than assume.
  2. Until then, fix the first half of the chain: run npm audit, remove or upgrade anything with a prototype-pollution finding, and freeze Object.prototype at process start in services where that is viable (Object.freeze(Object.prototype) breaks some libraries; measure).
  3. In CI, this is a reason to keep test jobs on short-lived, minimally scoped tokens — code running in a Vitest worker has whatever the job has — prevention/ci-cd-hardening.md.
  4. If a service that used tinypool per request also had an exploitable pollution, treat it as a code-execution incident: if-your-webapp-was-compromised.md, then rotating-cloud-credentials.md.

Prevention

  • Treat prototype-pollution findings as code execution until proven otherwise; gadgets like this one, in libraries nobody audits, are why — prevention/package-vetting-checklist.md.
  • When a security fix lands only in a new major of a transitive dependency, the fix has not reached you until the package that pins it moves; check npm ls for the installed version, not the advisory's "patched" line — prevention/npm-hardening.md.
  • Vendor advisories, CVE records and database listings for the same bug were six weeks apart here; the registry time field is the one date that does not move.

Why this matters for vibe coders

Vitest is the test runner agents reach for in generated TypeScript projects, and the CI job that runs it usually holds a deploy token. The chain is: a dependency the agent added has a prototype-pollution bug, a test imports a fixture that triggers it, tinypool spawns the next worker with an inherited --require, and the deploy token leaves the runner. None of the three pieces is exotic, and the fix for two of them is a major-version upgrade nobody asked the agent to do.

Sources