GitHub Advisoryadvisory · npm
GHSA-x9g3-xrwr-cwfg piscina: Prototype Pollution Gadget → RCE via inherited options.filename
HIGH
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