GHSA-x9g3-xrwr-cwfg piscina: Prototype Pollution Gadget → RCE via inherited options.filename
Summary`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 — 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)`.ImpactTwo preconditions:**Upstream PP-source** somewhere in the process — common in transitive deps**Attacker-controllable `.mjs`** at a known filesystem path — realistic via upload endpoints, /tmp races, predictable node_modules paths, or supply-chainOnce both fire:Every `pool.run(task, opts)` call across the entire process is hijackedAttacker'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 restartStrictly 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 versionsEmpirically verified vulnerable on `piscina@5.1.4` (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 conceptA) Minimal in-process PoCB) Full-stack HTTP chain (this is the realistic shape)A correctly-initialized pool gets hijacked by attacker activity. Pool is created at server boot with a legitimate worker, then per-request handlers call `pool.run(req.body, {signal: ac.signal})` — the standard abort-aware shape.Empirical observation on `piscina@5.1.4` + Node 23.11.0:Pre-attack `/api/process` returns `{by: 'valid-worker'}`Cold-path `/probe` after PP source confirms `({}).filename` is polluted process-widePost-attack `/api/process` returns `{by: 'ATTACKER-WORKER', processed: <caller's exfil data>}`Sentinel file written from inside `piscina/dist/worker.js` with the worker process's uid + env accessRecommended fixMinimal — own-property guard at both option-read sites:More idiomatic — use a null-prototype working object throughout `this.options`: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.CoordinationSame 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 discoveredGeneralized 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 separate disclosure.
CVSS v3.1 score: 8.1