Tinypool: プロトタイプ汚染により、`run()`オプションでリモートコード実行が可能になる脆弱性
`tinypool`は`piscina`の派生であり、同じプロトタイプ汚染の脆弱性を受け継いでいます。`pool.run(task, options)`が呼び出される際、`filename`オプションが提供された`options`オブジェクトから読み込まれます。もしそのオブジェクトに独自の`filename`プロパティが存在しない場合、`Object.prototype`を参照することになります。
攻撃者が`Object.prototype.filename`を汚染できる場合(例えば、脆弱な`lodash.merge`や`qs.parse`など、アプリケーション内の他の場所で)、tinypoolは攻撃者が制御するワーカーモジュールをロードして実行する可能性があります。
これは、piscinaにおけるルートディレクトリの発見に関する脆弱性[GHSA-x9g3-xrwr-cwfg](https://github.com/piscinajs/piscina/security/advisories/GHSA-x9g3-xrwr-cwfg)に対応するものです。
`pool.run(task)`を第2引数なしで呼び出した場合は影響を受けません。なぜなら、`kDefaultOptions.filename`は`null`であり、オプションオブジェクトがユーザーによって制御されないためです。この脆弱性は、呼び出し元が独自のオプションオブジェクトを`pool.run()`に渡した場合にのみ発生します。
## 影響
ワーカープール内での任意のJavaScript実行。アプリケーションが攻撃者が制御するデータを`run()`タスクとして渡し、さらに`run()`オプションオブジェクトも提供した場合、攻撃者は悪意のあるワーカーに実行をリダイレクトし、そのデータを外部に送信したり改ざんしたりすることで、リモートコード実行やデータ漏洩を達成する可能性があります。
## 実証コード
```js
// legitimate-worker.mjs
export default async (task) => ({ by: 'legitimate-worker', processed: task })
// malicious-worker.mjs
export default async (task) => ({ by: 'attacker', stolenRequestBody: task })
// main.js
import express from "express";
import Tinypool from "tinypool";
import { fileURLToPath } from "node:url";
import path from "node:path";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// 上流のプロトタイプ汚染をシミュレート (lodash merge, qs parseなど)
Object.prototype.filename = path.join(__dirname, "malicious-worker.mjs");
```
```
const pool = new Tinypool({
filename: path.join(__dirname, "legitimate-worker.mjs"),
});
express()
.use(express.json())
.post("/", async (req, res) => {
const ac = new AbortController();
const result = await pool.run(req.body, { signal: ac.signal });
res.json(result);
})
.listen(31337);
```
## 提案された修正点
ユーザーから提供されたすべてのオプションを、自身のプロパティからのみ読み込みます(`Object.hasOwn` または `Object.prototype.hasOwnProperty.call`を使用)。そして、内部の `ThreadPool.options` オブジェクトを、null プロトタイプで構築します。
影響を受けるパッケージ:
- npm (JavaScript) tinypool: 影響あり (バージョン 2.1.2 未満)、修正済み (バージョン 2.1.2)
```