VYPR
Low severityNVD Advisory· Published Aug 7, 2025· Updated Nov 3, 2025

tmp does not restrict arbitrary temporary file / directory write via symbolic link `dir` parameter

CVE-2025-54798

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.

PackageAffected versionsPatched versions
tmpnpm
< 0.2.40.2.4

Affected products

1
  • raszi/node-tmpv5
    Range: < 0.2.4

Patches

1
188b25e52949

Fix GHSA-52f5-9888-hmc6

https://github.com/raszi/node-tmpKARASZI IstvánApr 15, 2025via ghsa
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

News mentions

0

No linked articles in our index yet.