CVE-2026-49982
Description
Type-confusion bypass in tmp@0.2.6 allows path traversal via non-string prefix/postfix/template, leading to arbitrary file creation outside tmpdir.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Type-confusion bypass in tmp@0.2.6 allows path traversal via non-string prefix/postfix/template, leading to arbitrary file creation outside tmpdir.
Vulnerability
In tmp version 0.2.6, the _assertPath guard only rejects string values containing ... However, when prefix, postfix, or template is supplied as a non-string value (e.g., Array, Buffer, or any object), the includes('..') check returns falsy, but the value's stringification (via Array.prototype.join or String coercion) still produces a path containing ../. This bypasses the guard and allows path traversal. The affected functions are tmp.file, tmp.fileSync, tmp.dir, tmp.dirSync, tmp.tmpName, and tmp.tmpNameSync. [1]
Exploitation
An attacker can send untrusted request data (e.g., JSON body fields or query strings like ?prefix[]=..) to an application that forwards it directly to tmp functions without type coercion. The non-string value bypasses the _assertPath check, and the resulting stringified path escapes the intended temporary directory. No authentication or special privileges are required if the application exposes such endpoints. [1]
Impact
Successful exploitation allows the attacker to create files or directories at arbitrary locations on the filesystem with the host process's privileges. This can lead to writing malicious content into source trees, build outputs, or web roots, potentially enabling further compromise. In multi-tenant services, it crosses tenant boundaries. [1]
Mitigation
The vulnerability is fixed in tmp version 0.2.7. Users should upgrade to 0.2.7 or later. If immediate upgrade is not possible, applications should explicitly coerce user-supplied values to strings before passing them to tmp functions. [1]
AI Insight generated on Jun 11, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2Patches
1ce787f37aaacReject non-string prefix, postfix, template
2 files changed · +105 −9
lib/tmp.js+24 −9 modified@@ -526,16 +526,26 @@ function _generateTmpName(opts) { } /** - * Check the prefix and postfix options + * Check the prefix, postfix, and template options. + * + * Rejects non-string inputs so that a non-string `.includes('..')` cannot evade + * the substring check (e.g. an Array whose `.includes('..')` is element-wise, + * or a duck-typed object with a custom `.includes`), and so that the value is + * not later coerced to a string with traversal sequences via `Array.prototype.join` + * or `path.join`. * * @private */ -function _assertPath(path) { - if (path.includes("..")) { +function _assertPath(option, value) { + if (typeof value !== 'string') { + throw new Error(`${option} option must be a string, got "${typeof value}".`); + } + + if (value.includes("..")) { throw new Error("Relative value not allowed"); } - return path; + return value; } /** @@ -558,8 +568,13 @@ function _assertOptionsBase(options) { } /* istanbul ignore else */ - if (!_isUndefined(options.template) && !options.template.match(TEMPLATE_PATTERN)) { - throw new Error(`Invalid template, found "${options.template}".`); + if (!_isUndefined(options.template)) { + if (typeof options.template !== 'string') { + throw new Error(`template option must be a string, got "${typeof options.template}".`); + } + if (!options.template.match(TEMPLATE_PATTERN)) { + throw new Error(`Invalid template, found "${options.template}".`); + } } /* istanbul ignore else */ @@ -575,9 +590,9 @@ function _assertOptionsBase(options) { options.unsafeCleanup = !!options.unsafeCleanup; // for completeness' sake only, also keep (multiple) blanks if the user, purportedly sane, requests us to - options.prefix = _isUndefined(options.prefix) ? '' : _assertPath(options.prefix); - options.postfix = _isUndefined(options.postfix) ? '' : _assertPath(options.postfix); - options.template = _isUndefined(options.template) ? undefined : _assertPath(options.template); + options.prefix = _isUndefined(options.prefix) ? '' : _assertPath('prefix', options.prefix); + options.postfix = _isUndefined(options.postfix) ? '' : _assertPath('postfix', options.postfix); + options.template = _isUndefined(options.template) ? undefined : _assertPath('template', options.template); } /**
test/GHSA-7c78-jf6q-g5cm-test.js+81 −0 added@@ -0,0 +1,81 @@ +const assert = require('assert'); +const tmp = require('../lib/tmp'); + +describe('GHSA-7c78-jf6q-g5cm', function () { + describe('#fileSync with non-string `prefix`', function () { + it('should reject an array prefix even when its element is "../foo"', function (done) { + assert.throws(function () { + tmp.fileSync({ prefix: ['../foo'] }); + }, new RegExp('^Error: prefix option must be a string')); + + done(); + }); + + it('should reject a duck-typed object whose includes() returns false', function (done) { + assert.throws(function () { + tmp.fileSync({ + prefix: { toString: function () { return '../foo'; }, includes: function () { return false; } } + }); + }, new RegExp('^Error: prefix option must be a string')); + + done(); + }); + + it('should reject a number prefix', function (done) { + assert.throws(function () { + tmp.fileSync({ prefix: 42 }); + }, new RegExp('^Error: prefix option must be a string')); + + done(); + }); + }); + + describe('#fileSync with non-string `postfix`', function () { + it('should reject an array postfix', function (done) { + assert.throws(function () { + tmp.fileSync({ postfix: ['/../foo'] }); + }, new RegExp('^Error: postfix option must be a string')); + + done(); + }); + }); + + describe('#fileSync with non-string `template`', function () { + it('should reject an array template', function (done) { + assert.throws(function () { + tmp.fileSync({ template: ['XXXXXX/../foo'] }); + }, new RegExp('^Error: template option must be a string')); + + done(); + }); + }); + + describe('#dirSync with non-string `prefix`', function () { + it('should reject an array prefix', function (done) { + assert.throws(function () { + tmp.dirSync({ prefix: ['../escape'] }); + }, new RegExp('^Error: prefix option must be a string')); + + done(); + }); + }); + + describe('#tmpNameSync with non-string `prefix`', function () { + it('should reject an array prefix', function (done) { + assert.throws(function () { + tmp.tmpNameSync({ prefix: ['../escape'] }); + }, new RegExp('^Error: prefix option must be a string')); + + done(); + }); + }); + + describe('valid string prefixes still work', function () { + it('should accept a normal string prefix', function (done) { + const r = tmp.fileSync({ prefix: 'safe-prefix' }); + assert.ok(r.name.indexOf('safe-prefix') !== -1); + r.removeCallback(); + done(); + }); + }); +});
Vulnerability mechanics
Root cause
"Type-confusion in _assertPath: it calls .includes('..') on the argument assuming it is a string, but when prefix, postfix, or template is an Array or duck-typed object, the includes check is bypassed while the value is later coerced to a string containing ../ during path construction."
Attack vector
An attacker supplies a non-string value (Array, Buffer, or duck-typed object) for the prefix, postfix, or template option. Because _assertPath calls .includes('..') directly on the value, an Array's element-wise includes returns false for ['../escape'].includes('..'), and a duck-typed object with a custom includes() returning false also defeats the check [ref_id=1]. The value then flows through Array.prototype.join inside _generateTmpName and path.join(tmpDir, opts.dir, name), which coerce it to a string that still contains ../, producing a final path that escapes tmpdir [ref_id=1]. This is reachable from any application that forwards untrusted request data (e.g. 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 [ref_id=1].
Affected code
The vulnerable function is _assertPath in lib/tmp.js (tag v0.2.6, commit 41f7159), which calls path.includes('..') without first verifying that path is a string [ref_id=1]. The callers in _assertOptionsBase pass options.prefix, options.postfix, and options.template directly into _assertPath [ref_id=1]. The stringification occurs inside _generateTmpName via Array.prototype.join and path.join(tmpDir, opts.dir, name) [ref_id=1].
What the fix does
The patch modifies _assertPath to accept two arguments (option name and value) and adds a typeof value !== 'string' guard that throws an error before the .includes('..') check is reached [patch_id=5619099]. The same type check is also applied to the template option ahead of the TEMPLATE_PATTERN regex match, preventing a TypeError from .match() on a non-string [patch_id=5619099]. The call sites in _assertOptionsBase are updated to pass the option name ('prefix', 'postfix', 'template') so the error message identifies which option was invalid [patch_id=5619099].
Preconditions
- inputThe application must pass untrusted user input (e.g. from JSON body fields or qs-parsed query strings) as the prefix, postfix, or template option to tmp.file, tmp.fileSync, tmp.dir, tmp.dirSync, tmp.tmpName, or tmp.tmpNameSync without type coercion.
- inputThe attacker must be able to supply a non-string value (Array, Buffer, or object) whose string representation contains '../' sequences.
- authNo authentication is required; the vulnerability is reachable by any network client that can send requests to the affected application endpoint.
Reproduction
1. Install the vulnerable package: `npm install tmp@0.2.6` and create a script that calls `tmp.fileSync({ tmpdir: baseDir, prefix: ['../escape'] })`. 2. Observe that the file is created outside `baseDir` (e.g. at `/private/tmp/escape-...`) despite the `_assertPath` guard [ref_id=1]. 3. For a realistic Express scenario, run the victim-server.js from the advisory, then send `curl -s -X POST -H 'Content-Type: application/json' -d '{"prefix":["../escape-array"]}' http://127.0.0.1:3000/upload` and confirm the response shows `"escaped":true` [ref_id=1].
Generated on Jun 11, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
1News mentions
0No linked articles in our index yet.