tmp does not restrict arbitrary temporary file / directory write via symbolic link `dir` parameter
Description
tmp is a temporary file and directory creator for node.js. In versions 0.2.3 and below, tmp is vulnerable to an arbitrary temporary file / directory write via symbolic link dir parameter. This is fixed in version 0.2.4.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
tmp library for Node.js allows arbitrary file write via symlink dir parameter, fixed in 0.2.4.
Root
Cause The vulnerability stems from the _resolvePath function in tmp that fails to properly handle symbolic links. When the dir parameter is provided, the function checks if the path starts with the system temporary directory (tmpDir), but it does not resolve symlinks before comparison. This allows an attacker to supply a symlink that points to a location outside tmpDir, bypassing the relative path assertion [3].
Exploitation
An attacker can control the dir option passed to tmp functions. By setting dir to a symlink that resolves to an arbitrary filesystem path, the library will proceed to create temporary files or directories at that resolved location. No authentication is required beyond the ability to pass options to the library, making it exploitable in scenarios where user input influences the dir parameter [3].
Impact
Successful exploitation enables arbitrary temporary file and directory writes to any location the application process can write to. This could result in overwriting sensitive files, placing malicious files, or potentially achieving code execution if combined with other weaknesses [3].
Mitigation
The issue has been patched in tmp version 0.2.4 [4]. Users of affected versions (0.2.3 and below) should upgrade immediately. The fix resolves symlinks during path validation to ensure the destination remains within the intended temporary directory.
AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
tmpnpm | < 0.2.4 | 0.2.4 |
Affected products
1- raszi/node-tmpv5Range: < 0.2.4
Patches
1188b25e52949Fix GHSA-52f5-9888-hmc6
1 file changed · +192 −134
lib/tmp.js+192 −134 modified@@ -18,34 +18,24 @@ const _c = { fs: fs.constants, os: os.constants }; /* * The working inner variables. */ -const - // the random characters to choose from +const // the random characters to choose from RANDOM_CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', - TEMPLATE_PATTERN = /XXXXXX/, - DEFAULT_TRIES = 3, - CREATE_FLAGS = (_c.O_CREAT || _c.fs.O_CREAT) | (_c.O_EXCL || _c.fs.O_EXCL) | (_c.O_RDWR || _c.fs.O_RDWR), - // constants are off on the windows platform and will not match the actual errno codes IS_WIN32 = os.platform() === 'win32', EBADF = _c.EBADF || _c.os.errno.EBADF, ENOENT = _c.ENOENT || _c.os.errno.ENOENT, - DIR_MODE = 0o700 /* 448 */, FILE_MODE = 0o600 /* 384 */, - EXIT = 'exit', - // this will hold the objects need to be removed on exit _removeObjects = [], - // API change in fs.rmdirSync leads to error when passing in a second parameter, e.g. the callback FN_RMDIR_SYNC = fs.rmdirSync.bind(fs); -let - _gracefulCleanup = false; +let _gracefulCleanup = false; /** * Recursively remove a directory and its contents. @@ -75,38 +65,35 @@ function FN_RIMRAF_SYNC(dirPath) { * @param {?tmpNameCallback} callback the callback function */ function tmpName(options, callback) { - const - args = _parseArguments(options, callback), + const args = _parseArguments(options, callback), opts = args[0], cb = args[1]; - try { - _assertAndSanitizeOptions(opts); - } catch (err) { - return cb(err); - } + _assertAndSanitizeOptions(opts, function (err, sanitizedOptions) { + if (err) return cb(err); - let tries = opts.tries; - (function _getUniqueName() { - try { - const name = _generateTmpName(opts); + let tries = sanitizedOptions.tries; + (function _getUniqueName() { + try { + const name = _generateTmpName(sanitizedOptions); - // check whether the path exists then retry if needed - fs.stat(name, function (err) { - /* istanbul ignore else */ - if (!err) { + // check whether the path exists then retry if needed + fs.stat(name, function (err) { /* istanbul ignore else */ - if (tries-- > 0) return _getUniqueName(); + if (!err) { + /* istanbul ignore else */ + if (tries-- > 0) return _getUniqueName(); - return cb(new Error('Could not get a unique tmp filename, max tries reached ' + name)); - } + return cb(new Error('Could not get a unique tmp filename, max tries reached ' + name)); + } - cb(null, name); - }); - } catch (err) { - cb(err); - } - }()); + cb(null, name); + }); + } catch (err) { + cb(err); + } + })(); + }); } /** @@ -117,15 +104,14 @@ function tmpName(options, callback) { * @throws {Error} if the options are invalid or could not generate a filename */ function tmpNameSync(options) { - const - args = _parseArguments(options), + const args = _parseArguments(options), opts = args[0]; - _assertAndSanitizeOptions(opts); + const sanitizedOptions = _assertAndSanitizeOptionsSync(opts); - let tries = opts.tries; + let tries = sanitizedOptions.tries; do { - const name = _generateTmpName(opts); + const name = _generateTmpName(sanitizedOptions); try { fs.statSync(name); } catch (e) { @@ -143,8 +129,7 @@ function tmpNameSync(options) { * @param {?fileCallback} callback */ function file(options, callback) { - const - args = _parseArguments(options, callback), + const args = _parseArguments(options, callback), opts = args[0], cb = args[1]; @@ -181,13 +166,12 @@ function file(options, callback) { * @throws {Error} if cannot create a file */ function fileSync(options) { - const - args = _parseArguments(options), + const args = _parseArguments(options), opts = args[0]; const discardOrDetachDescriptor = opts.discardDescriptor || opts.detachDescriptor; const name = tmpNameSync(opts); - var fd = fs.openSync(name, CREATE_FLAGS, opts.mode || FILE_MODE); + let fd = fs.openSync(name, CREATE_FLAGS, opts.mode || FILE_MODE); /* istanbul ignore else */ if (opts.discardDescriptor) { fs.closeSync(fd); @@ -208,8 +192,7 @@ function fileSync(options) { * @param {?dirCallback} callback */ function dir(options, callback) { - const - args = _parseArguments(options, callback), + const args = _parseArguments(options, callback), opts = args[0], cb = args[1]; @@ -236,8 +219,7 @@ function dir(options, callback) { * @throws {Error} if it cannot create a directory */ function dirSync(options) { - const - args = _parseArguments(options), + const args = _parseArguments(options), opts = args[0]; const name = tmpNameSync(opts); @@ -288,8 +270,7 @@ function _removeFileSync(fdPath) { } finally { try { fs.unlinkSync(fdPath[1]); - } - catch (e) { + } catch (e) { // reraise any unanticipated error if (!_isENOENT(e)) rethrownException = e; } @@ -361,7 +342,6 @@ function _prepareRemoveCallback(removeFunction, fileOrDirName, sync, cleanupCall // if sync is true, the next parameter will be ignored return function _cleanupCallback(next) { - /* istanbul ignore else */ if (!called) { // remove cleanupCallback from cache @@ -374,7 +354,7 @@ function _prepareRemoveCallback(removeFunction, fileOrDirName, sync, cleanupCall if (sync || removeFunction === FN_RMDIR_SYNC || removeFunction === FN_RIMRAF_SYNC) { return removeFunction(fileOrDirName); } else { - return removeFunction(fileOrDirName, next || function() {}); + return removeFunction(fileOrDirName, next || function () {}); } } }; @@ -409,8 +389,7 @@ function _garbageCollector() { * @private */ function _randomChars(howMany) { - let - value = [], + let value = [], rnd = null; // make sure that we do not fail because we ran out of entropy @@ -420,24 +399,13 @@ function _randomChars(howMany) { rnd = crypto.pseudoRandomBytes(howMany); } - for (var i = 0; i < howMany; i++) { + for (let i = 0; i < howMany; i++) { value.push(RANDOM_CHARS[rnd[i] % RANDOM_CHARS.length]); } return value.join(''); } -/** - * Helper which determines whether a string s is blank, that is undefined, or empty or null. - * - * @private - * @param {string} s - * @returns {Boolean} true whether the string s is blank, false otherwise - */ -function _isBlank(s) { - return s === null || _isUndefined(s) || !s.trim(); -} - /** * Checks whether the `obj` parameter is defined or not. * @@ -479,6 +447,51 @@ function _parseArguments(options, callback) { return [actualOptions, callback]; } +/** + * Resolve the specified path name in respect to tmpDir. + * + * The specified name might include relative path components, e.g. ../ + * so we need to resolve in order to be sure that is is located inside tmpDir + * + * @private + */ +function _resolvePath(name, tmpDir, cb) { + const pathToResolve = path.isAbsolute(name) ? name : path.join(tmpDir, name); + + fs.stat(pathToResolve, function (err) { + if (err) { + fs.realpath(path.dirname(pathToResolve), function (err, parentDir) { + if (err) return cb(err); + + cb(null, path.join(parentDir, path.basename(pathToResolve))); + }); + } else { + fs.realpath(path, cb); + } + }); +} + +/** + * Resolve the specified path name in respect to tmpDir. + * + * The specified name might include relative path components, e.g. ../ + * so we need to resolve in order to be sure that is is located inside tmpDir + * + * @private + */ +function _resolvePathSync(name, tmpDir) { + const pathToResolve = path.isAbsolute(name) ? name : path.join(tmpDir, name); + + try { + fs.statSync(pathToResolve); + return fs.realpathSync(pathToResolve); + } catch (_err) { + const parentDir = fs.realpathSync(path.dirname(pathToResolve)); + + return path.join(parentDir, path.basename(pathToResolve)); + } +} + /** * Generates a new temporary name. * @@ -487,16 +500,17 @@ function _parseArguments(options, callback) { * @private */ function _generateTmpName(opts) { - const tmpDir = opts.tmpdir; /* istanbul ignore else */ - if (!_isUndefined(opts.name)) + if (!_isUndefined(opts.name)) { return path.join(tmpDir, opts.dir, opts.name); + } /* istanbul ignore else */ - if (!_isUndefined(opts.template)) + if (!_isUndefined(opts.template)) { return path.join(tmpDir, opts.dir, opts.template).replace(TEMPLATE_PATTERN, _randomChars(6)); + } // prefix and postfix const name = [ @@ -512,33 +526,32 @@ function _generateTmpName(opts) { } /** - * Asserts whether the specified options are valid, also sanitizes options and provides sane defaults for missing - * options. + * Asserts and sanitizes the basic options. * - * @param {Options} options * @private */ -function _assertAndSanitizeOptions(options) { +function _assertOptionsBase(options) { + if (!_isUndefined(options.name)) { + const name = options.name; - options.tmpdir = _getTmpDir(options); + // assert that name is not absolute and does not contain a path + if (path.isAbsolute(name)) throw new Error(`name option must not contain an absolute path, found "${name}".`); - const tmpDir = options.tmpdir; + // must not fail on valid .<name> or ..<name> or similar such constructs + const basename = path.basename(name); + if (basename === '..' || basename === '.' || basename !== name) + throw new Error(`name option must not contain a path, found "${name}".`); + } /* istanbul ignore else */ - if (!_isUndefined(options.name)) - _assertIsRelative(options.name, 'name', tmpDir); - /* istanbul ignore else */ - if (!_isUndefined(options.dir)) - _assertIsRelative(options.dir, 'dir', tmpDir); - /* istanbul ignore else */ - if (!_isUndefined(options.template)) { - _assertIsRelative(options.template, 'template', tmpDir); - if (!options.template.match(TEMPLATE_PATTERN)) - throw new Error(`Invalid template, found "${options.template}".`); + if (!_isUndefined(options.template) && !options.template.match(TEMPLATE_PATTERN)) { + throw new Error(`Invalid template, found "${options.template}".`); } + /* istanbul ignore else */ - if (!_isUndefined(options.tries) && isNaN(options.tries) || options.tries < 0) + if ((!_isUndefined(options.tries) && isNaN(options.tries)) || options.tries < 0) { throw new Error(`Invalid tries, found "${options.tries}".`); + } // if a name was specified we will try once options.tries = _isUndefined(options.name) ? options.tries || DEFAULT_TRIES : 1; @@ -547,65 +560,103 @@ function _assertAndSanitizeOptions(options) { options.discardDescriptor = !!options.discardDescriptor; options.unsafeCleanup = !!options.unsafeCleanup; - // sanitize dir, also keep (multiple) blanks if the user, purportedly sane, requests us to - options.dir = _isUndefined(options.dir) ? '' : path.relative(tmpDir, _resolvePath(options.dir, tmpDir)); - options.template = _isUndefined(options.template) ? undefined : path.relative(tmpDir, _resolvePath(options.template, tmpDir)); - // sanitize further if template is relative to options.dir - options.template = _isBlank(options.template) ? undefined : path.relative(options.dir, options.template); - // for completeness' sake only, also keep (multiple) blanks if the user, purportedly sane, requests us to - options.name = _isUndefined(options.name) ? undefined : options.name; options.prefix = _isUndefined(options.prefix) ? '' : options.prefix; options.postfix = _isUndefined(options.postfix) ? '' : options.postfix; } /** - * Resolve the specified path name in respect to tmpDir. + * Gets the relative directory to tmpDir. * - * The specified name might include relative path components, e.g. ../ - * so we need to resolve in order to be sure that is is located inside tmpDir + * @private + */ +function _getRelativePath(option, name, tmpDir, cb) { + if (_isUndefined(name)) return cb(null); + + _resolvePath(name, tmpDir, function (err, resolvedPath) { + if (err) return cb(err); + + const relativePath = path.relative(tmpDir, resolvedPath); + + if (!resolvedPath.startsWith(tmpDir)) { + return cb(new Error(`${option} option must be relative to "${tmpDir}", found "${relativePath}".`)); + } + + cb(null, relativePath); + }); +} + +/** + * Gets the relative path to tmpDir. * - * @param name - * @param tmpDir - * @returns {string} * @private */ -function _resolvePath(name, tmpDir) { - if (name.startsWith(tmpDir)) { - return path.resolve(name); - } else { - return path.resolve(path.join(tmpDir, name)); +function _getRelativePathSync(option, name, tmpDir) { + if (_isUndefined(name)) return; + + const resolvedPath = _resolvePathSync(name, tmpDir); + const relativePath = path.relative(tmpDir, resolvedPath); + + if (!resolvedPath.startsWith(tmpDir)) { + throw new Error(`${option} option must be relative to "${tmpDir}", found "${relativePath}".`); } + + return relativePath; } /** - * Asserts whether specified name is relative to the specified tmpDir. + * Asserts whether the specified options are valid, also sanitizes options and provides sane defaults for missing + * options. * - * @param {string} name - * @param {string} option - * @param {string} tmpDir - * @throws {Error} * @private */ -function _assertIsRelative(name, option, tmpDir) { - if (option === 'name') { - // assert that name is not absolute and does not contain a path - if (path.isAbsolute(name)) - throw new Error(`${option} option must not contain an absolute path, found "${name}".`); - // must not fail on valid .<name> or ..<name> or similar such constructs - let basename = path.basename(name); - if (basename === '..' || basename === '.' || basename !== name) - throw new Error(`${option} option must not contain a path, found "${name}".`); - } - else { // if (option === 'dir' || option === 'template') { - // assert that dir or template are relative to tmpDir - if (path.isAbsolute(name) && !name.startsWith(tmpDir)) { - throw new Error(`${option} option must be relative to "${tmpDir}", found "${name}".`); +function _assertAndSanitizeOptions(options, cb) { + _getTmpDir(options, function (err, tmpDir) { + if (err) return cb(err); + + options.tmpdir = tmpDir; + + try { + _assertOptionsBase(options, tmpDir); + } catch (err) { + return cb(err); } - let resolvedPath = _resolvePath(name, tmpDir); - if (!resolvedPath.startsWith(tmpDir)) - throw new Error(`${option} option must be relative to "${tmpDir}", found "${resolvedPath}".`); - } + + // sanitize dir, also keep (multiple) blanks if the user, purportedly sane, requests us to + _getRelativePath('dir', options.dir, tmpDir, function (err, dir) { + if (err) return cb(err); + + options.dir = _isUndefined(dir) ? '' : dir; + + // sanitize further if template is relative to options.dir + _getRelativePath('template', options.template, tmpDir, function (err, template) { + if (err) return cb(err); + + options.template = template; + + cb(null, options); + }); + }); + }); +} + +/** + * Asserts whether the specified options are valid, also sanitizes options and provides sane defaults for missing + * options. + * + * @private + */ +function _assertAndSanitizeOptionsSync(options) { + const tmpDir = (options.tmpdir = _getTmpDirSync(options)); + + _assertOptionsBase(options, tmpDir); + + const dir = _getRelativePathSync('dir', options.dir, tmpDir); + options.dir = _isUndefined(dir) ? '' : dir; + + options.template = _getRelativePathSync('template', options.template, tmpDir); + + return options; } /** @@ -663,11 +714,18 @@ function setGracefulCleanup() { * Returns the currently configured tmp dir from os.tmpdir(). * * @private - * @param {?Options} options - * @returns {string} the currently configured tmp dir */ -function _getTmpDir(options) { - return path.resolve(options && options.tmpdir || os.tmpdir()); +function _getTmpDir(options, cb) { + return fs.realpath((options && options.tmpdir) || os.tmpdir(), cb); +} + +/** + * Returns the currently configured tmp dir from os.tmpdir(). + * + * @private + */ +function _getTmpDirSync(options) { + return fs.realpathSync((options && options.tmpdir) || os.tmpdir()); } // Install process exit listener @@ -768,7 +826,7 @@ Object.defineProperty(module.exports, 'tmpdir', { enumerable: true, configurable: false, get: function () { - return _getTmpDir(); + return _getTmpDirSync(); } });
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-52f5-9888-hmc6ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-54798ghsaADVISORY
- github.com/raszi/node-tmp/commit/188b25e529496e37adaf1a1d9dccb40019a08b1bghsax_refsource_MISCWEB
- github.com/raszi/node-tmp/issues/207ghsax_refsource_MISCWEB
- github.com/raszi/node-tmp/security/advisories/GHSA-52f5-9888-hmc6ghsax_refsource_CONFIRMWEB
- lists.debian.org/debian-lts-announce/2025/08/msg00007.htmlghsaWEB
News mentions
0No linked articles in our index yet.