GHSA-7c78-jf6q-g5cm: tmp: Type-confusion bypass of _assertPath allows path traversal via non-string prefix/postfix/template
SummaryThe `_assertPath` guard added to `tmp@0.2.6` rejects only string values that contain the substring `..`. It is bypassed when `prefix`, `postfix`, or `template` is supplied as a non-string value (Array, Buffer, or any object) whose `includes('..')` returns falsy but whose stringification still contains `../`. The value flows through `Array.prototype.join`/`String` coercion inside `_generateTmpName` and `path.join(tmpDir, opts.dir, name)`, producing a final path that escapes `tmpdir` and creates a file or directory at an attacker-controlled location with the host process's privileges.This affects any application that forwards untrusted request data (a common pattern is JSON body fields or `qs`-parsed bracket-array query strings such as `?prefix[]=...`) into `tmp.file`, `tmp.fileSync`, `tmp.dir`, `tmp.dirSync`, `tmp.tmpName`, or `tmp.tmpNameSync` without explicit type coercion.ImpactArbitrary file creation outside the intended temporary directory, with the running process's filesystem permissions.Directory creation outside the intended tree (via `tmp.dir{,Sync}`), which can then host a subsequent symlink swap.File content that the application writes to the returned descriptor lands at the attacker's chosen path. In multi-tenant services this crosses tenant boundaries; in CI/build systems it can write into source trees, build outputs, or web roots.CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:L - score 8.1 (High). Network-reachable when the consumer passes request data unchanged.Affected versions`tmp` >= 0.2.6 (the `_assertPath` guard introduced by commit 7ef2728 / merged in efa4a06f). Earlier releases are vulnerable to the plain string form (already published as a separate advisory) plus this bypass.Vulnerable code`lib/tmp.js` at tag `v0.2.6`, commit 41f7159:Root cause: `_assertPath` assumes its argument is a string. For an `Array` argument, `Array.prototype.includes('..')` checks element equality (so `['../escape'].includes('..')` is `false`); for an arbitrary object, `Object.prototype.includes` does not exist and a duck-typed `includes: () => false` defeats the check entirely. In both shapes, the subsequent `[...].join('')` and `path.join(...)` coerce the value to its underlying string, which still contains `../`.How untrusted data reaches `_assertPath`Two production-realistic shapes that yield a non-string `prefix`/`postfix`/`template`:JSON request bodies. `express.json()` (and any other JSON body parser) preserves the parsed value's type. A body of `{"prefix":["../escape"]}` reaches the handler as an Array.`qs`-style bracket-array query strings. Express 4's default `qs` parser turns `?prefix[]=../escape` into `['../escape']`. The same applies to any framework using `qs` (Fastify, Koa with bodyparser, Hapi via configured parsers, etc.).The consumer pattern is the natural one - forward `req.body.prefix` directly into `tmp.file({ prefix, tmpdir })` with no developer-side coercion. The 0.2.6 release notes describe the guard as preventing prefix/postfix traversal, so consumers reasonably believe the guard covers the typical input flow.Proof of concept (string vs array)`poc.js` (run after `npm install tmp@0.2.6`):Observed output on `tmp@0.2.6`:End-to-end reproduction (against the deployed npm package)Install:`victim-server.js` - realistic Express app that forwards a JSON body field into `tmp.file`:Run:Drive three requests from another shell:Captured transcript (verbatim from the test rig):Server log:Observations:ATTACK 1 (string `../escape-string`) is rejected at `_assertPath`. The 0.2.6 guard works for plain strings.ATTACK 2 (array `["../escape-array"]`) passes the guard and creates a file at `/private/tmp/escape-array-...`, outside the tenant base `/tmp/tenant-base-3XHwPZ`. The file content is `attacker-controlled-content`. Confirmed with `ls`:Tenant base is empty. The escape is complete.ATTACK 3 (array `["../../../etc/poc-tmp-bypass"]`) reaches `fs.open` for `/etc/poc-tmp-bypass-...`. The open fails only because of POSIX permissions, not because tmp blocked the path. On a process running as root, or against any world-writable target directory, this would succeed.Negative control with patched buildApplying the suggested fix below and re-running ATTACK 2:The patched build rejects non-string `prefix`/`postfix`/`template` with a clear type error before the path is constructed.Suggested fixPatch `_assertPath` to require a string argument. The check `value.includes('..')` is sound only over strings; any non-string with a custom or array-element `includes` semantics bypasses it.Defence-in-depth, recommended in addition to the type check: validate the final resolved path against `tmpdir` after `_generateTmpName`, similar to what `_getRelativePath` already does for `dir` and `template`. That way any future bypass through a different vector (e.g., a future Node `path` change, or a different option) does not exit `tmpdir`.Fix PRhttps://github.com/raszi/node-tmp-ghsa-7c78-jf6q-g5cm/pull/1CreditReported by tonghuaroot.
CVSS v3.1: 8.2