GitHub Advisoryadvisory · npm

GHSA-x9g3-xrwr-cwfg piscina: Prototype Pollution Gadget → RCE via inherited options.filename

HIGH Published 2026-06-18 Modified 2026-06-18 Official page ↗
Aliases
CVE-2026-55388
Ecosystem
npm
Packages
piscina
Severity
High
CVSS vector
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
Weaknesses
CWE-1321, CWE-94
Reviewed
2026-06-18

Affected packages

• npm piscina: >= 5.0.0-alpha.0, < 5.2.0 → fixed in 5.2.0 • npm piscina: < 4.9.3 → fixed in 4.9.3 • npm piscina: >= 6.0.0-rc.1, < 6.0.0-rc.2 → fixed in 6.0.0-rc.2

Description

Summary `piscina`'s constructor and `run()` paths read the `filename` option via plain member access: ```js // 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 { vulnMerge(config, req.body); // PP source res.json({ ok: true }); }); app.post('/api/process', express.json(), async (req, res) => { const ac = new AbortController(); const result = await pool.run(req.body, { signal: ac.signal }); // }` • Sentinel file written from inside `piscina/dist/worker.js` with the worker process's uid + env access Recommended fix Minimal — own-property guard at both option-read sites: ```js // constructor (line 92) const userFilename = Object.prototype.hasOwnProperty.call(options, 'filename') ? options.filename : null; const filename = userFilename ? (0, common_1.maybeFileURLToPath)(userFilename) : null; // run() (line 616) const safeOpts = Object.create(null); Object.assign(safeOpts, options); // copies own props only? — keeps shape const { transferList, filename, name, signal } = safeOpts; ``` More idiomatic — use a null-prototype working object throughout `this.options`: ```js const safeOpts = Object.create(null); Object.assign(safeOpts, kDefaultOptions, options); this.options = safeOpts; this.options.filename = safeOpts.filename ? (0, common_1.maybeFileURLToPath)(safeOpts.filename) : null; this.options.maxQueue = 0; ``` Either approach closes the gadget without breaking any legitimate caller pattern. The pattern is the same as recommended for axios CVE-2026-44494 and the pino PSA filed earlier today. Cross-fix consideration: any other library you maintain that uses similar `options.X` member-access for worker / child-process / module-load operations is worth a quick audit. Coordination • Same maintainer as pino — you're already in security-triage mode for that PSA. Happy to coordinate timing / disclosure dates across both. • Will not share publicly until GHSA published or 90 days. • Please credit `ridingsa` if you choose to credit a reporter. How this was discovered Generalized the pino disclosure's mechanism — any library that reads a string option via plain member access and dynamic-loads it (via `import()` / `require()` / `new Worker()`) is a candidate. Ran a sweep across 10 candidate libraries; piscina + fastify (via pino propagation) fired. Piscina is independently vulnerable through its own option-read sites, hence this…

Related

Alias · CVE (1)

  • CVE-2026-55388 piscina is a node.js worker pool implementation. vulnerability

Weakness · CWE (2)

  • CWE-1321 Improperly Controlled Modification of Object Prototype Attributes weakness
  • CWE-94 Improper Control of Generation of Code weakness

References