Package: release.debian.org Severity: normal Tags: trixie X-Debbugs-Cc: node-...@packages.debian.org, secur...@debian.org Control: affects -1 + src:node-tmp User: release.debian....@packages.debian.org Usertags: pu
* CVE-2025-54798: Arbitrary file write (Closes: #1110532)
diffstat for node-tmp-0.2.2+dfsg+~0.2.3 node-tmp-0.2.2+dfsg+~0.2.3 changelog | 14 patches/0001-Fix-GHSA-52f5-9888-hmc6.patch | 543 ++++++++++++++++++ patches/0002-Fix-use-of-tmp.dir-with-dir-option.patch | 46 + patches/series | 2 4 files changed, 605 insertions(+) diff -Nru node-tmp-0.2.2+dfsg+~0.2.3/debian/changelog node-tmp-0.2.2+dfsg+~0.2.3/debian/changelog --- node-tmp-0.2.2+dfsg+~0.2.3/debian/changelog 2022-08-29 11:39:34.000000000 +0300 +++ node-tmp-0.2.2+dfsg+~0.2.3/debian/changelog 2025-08-17 19:11:35.000000000 +0300 @@ -1,3 +1,17 @@ +node-tmp (0.2.2+dfsg+~0.2.3-1.1~deb13u1) trixie; urgency=medium + + * Non-maintainer upload. + * Rebuild for trixie. + + -- Adrian Bunk <b...@debian.org> Sun, 17 Aug 2025 19:11:35 +0300 + +node-tmp (0.2.2+dfsg+~0.2.3-1.1) unstable; urgency=medium + + * Non-maintainer upload. + * CVE-2025-54798: Arbitrary file write (Closes: #1110532) + + -- Adrian Bunk <b...@debian.org> Sun, 10 Aug 2025 22:14:13 +0300 + node-tmp (0.2.2+dfsg+~0.2.3-1) unstable; urgency=medium * Team upload diff -Nru node-tmp-0.2.2+dfsg+~0.2.3/debian/patches/0001-Fix-GHSA-52f5-9888-hmc6.patch node-tmp-0.2.2+dfsg+~0.2.3/debian/patches/0001-Fix-GHSA-52f5-9888-hmc6.patch --- node-tmp-0.2.2+dfsg+~0.2.3/debian/patches/0001-Fix-GHSA-52f5-9888-hmc6.patch 1970-01-01 02:00:00.000000000 +0200 +++ node-tmp-0.2.2+dfsg+~0.2.3/debian/patches/0001-Fix-GHSA-52f5-9888-hmc6.patch 2025-08-10 22:14:13.000000000 +0300 @@ -0,0 +1,543 @@ +From f16ed9c304fae1ce14780608792928090a7e5fe3 Mon Sep 17 00:00:00 2001 +From: KARASZI István <ikara...@gmail.com> +Date: Wed, 16 Apr 2025 01:52:32 +0200 +Subject: Fix GHSA-52f5-9888-hmc6 + +--- + lib/tmp.js | 326 +++++++++++++++++++++++++++++++---------------------- + 1 file changed, 192 insertions(+), 134 deletions(-) + +diff --git a/lib/tmp.js b/lib/tmp.js +index aeba023..8b2ed3f 100644 +--- a/lib/tmp.js ++++ b/lib/tmp.js +@@ -19,35 +19,25 @@ const rimraf = require('rimraf'); + /* + * 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), + FN_RIMRAF_SYNC = rimraf.sync; + +-let +- _gracefulCleanup = false; ++let _gracefulCleanup = false; + + /** + * Gets a temporary file name. +@@ -56,38 +46,35 @@ let + * @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); ++ } ++ })(); ++ }); + } + + /** +@@ -98,15 +85,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) { +@@ -124,8 +110,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]; + +@@ -162,13 +147,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); +@@ -189,8 +173,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]; + +@@ -217,8 +200,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); +@@ -269,8 +251,7 @@ function _removeFileSync(fdPath) { + } finally { + try { + fs.unlinkSync(fdPath[1]); +- } +- catch (e) { ++ } catch (e) { + // reraise any unanticipated error + if (!_isENOENT(e)) rethrownException = e; + } +@@ -342,7 +323,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 +@@ -355,7 +335,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 () {}); + } + } + }; +@@ -390,8 +370,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 +@@ -401,24 +380,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. + * +@@ -460,6 +428,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. + * +@@ -468,16 +481,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 = [ +@@ -493,33 +507,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; +@@ -528,65 +541,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; + } + + /** +@@ -644,11 +695,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 +@@ -749,7 +807,7 @@ Object.defineProperty(module.exports, 'tmpdir', { + enumerable: true, + configurable: false, + get: function () { +- return _getTmpDir(); ++ return _getTmpDirSync(); + } + }); + +-- +2.30.2 + diff -Nru node-tmp-0.2.2+dfsg+~0.2.3/debian/patches/0002-Fix-use-of-tmp.dir-with-dir-option.patch node-tmp-0.2.2+dfsg+~0.2.3/debian/patches/0002-Fix-use-of-tmp.dir-with-dir-option.patch --- node-tmp-0.2.2+dfsg+~0.2.3/debian/patches/0002-Fix-use-of-tmp.dir-with-dir-option.patch 1970-01-01 02:00:00.000000000 +0200 +++ node-tmp-0.2.2+dfsg+~0.2.3/debian/patches/0002-Fix-use-of-tmp.dir-with-dir-option.patch 2025-08-10 22:14:13.000000000 +0300 @@ -0,0 +1,46 @@ +From 2bb58dee4e6dfc40046497b9f72da0598b64169b Mon Sep 17 00:00:00 2001 +From: fflorent <florent....@zeteo.me> +Date: Thu, 7 Aug 2025 19:53:53 +0200 +Subject: Fix use of tmp.dir() with `dir` option + +--- + lib/tmp.js | 2 +- + test/inband-standard.js | 2 ++ + 2 files changed, 3 insertions(+), 1 deletion(-) + +diff --git a/lib/tmp.js b/lib/tmp.js +index 8b2ed3f..bd57182 100644 +--- a/lib/tmp.js ++++ b/lib/tmp.js +@@ -447,7 +447,7 @@ function _resolvePath(name, tmpDir, cb) { + cb(null, path.join(parentDir, path.basename(pathToResolve))); + }); + } else { +- fs.realpath(path, cb); ++ fs.realpath(pathToResolve, cb); + } + }); + } +diff --git a/test/inband-standard.js b/test/inband-standard.js +index 05eb8b3..28e030f 100644 +--- a/test/inband-standard.js ++++ b/test/inband-standard.js +@@ -3,6 +3,7 @@ + + var + fs = require('fs'), ++ os = require('os'), + path = require('path'), + assertions = require('./assertions'), + rimraf = require('rimraf'), +@@ -19,6 +20,7 @@ module.exports = function inbandStandard(isFile, beforeHook, sync = false) { + describe('with mode', inbandStandardTests(null, { mode: 0o755 }, isFile, beforeHook, sync)); + describe('with multiple options', inbandStandardTests(null, { prefix: 'tmp-multiple', postfix: 'bar', mode: 0o750 }, isFile, beforeHook, sync)); + describe('with tmpdir option', inbandStandardTests(null, { tmpdir: path.join(tmp.tmpdir, 'tmp-external'), mode: 0o750 }, isFile, beforeHook, sync)); ++ describe('with dir option', inbandStandardTests(null, { dir: os.tmpdir(), mode: 0o750 }, isFile, beforeHook, sync)); + if (isFile) { + describe('with discardDescriptor', inbandStandardTests(null, { mode: testMode, discardDescriptor: true }, isFile, beforeHook, sync)); + describe('with detachDescriptor', inbandStandardTests(null, { mode: testMode, detachDescriptor: true }, isFile, beforeHook, sync)); +-- +2.30.2 + diff -Nru node-tmp-0.2.2+dfsg+~0.2.3/debian/patches/series node-tmp-0.2.2+dfsg+~0.2.3/debian/patches/series --- node-tmp-0.2.2+dfsg+~0.2.3/debian/patches/series 2020-10-14 09:58:18.000000000 +0300 +++ node-tmp-0.2.2+dfsg+~0.2.3/debian/patches/series 2025-08-10 22:14:13.000000000 +0300 @@ -1 +1,3 @@ disable-test-for-debci-machines.diff +0001-Fix-GHSA-52f5-9888-hmc6.patch +0002-Fix-use-of-tmp.dir-with-dir-option.patch
-- Pkg-javascript-devel mailing list Pkg-javascript-devel@alioth-lists.debian.net https://alioth-lists.debian.net/cgi-bin/mailman/listinfo/pkg-javascript-devel