Back to Blog
high SEVERITY7 min read

How Path Traversal happens in Node.js temporary file creation and how to fix it

CVE-2026-44705 is a high-severity path traversal vulnerability in the Node.js `tmp` package where unsanitized `prefix` and `postfix` options allow attackers to escape the intended temporary directory. Three separate nested copies of `tmp` — versions `0.0.28` and `0.2.7` pinned under `can-symlink`, `broccoli`, and `ember-template-recast` — were removed from `package-lock.json` and replaced by a single patched `0.2.6` resolution. The fix eliminates the directory-escape attack surface while leaving

O
By Orbis AppSec
Published August 26, 2026Reviewed August 26, 2026

Answer Summary

CVE-2026-44705 is a high-severity path traversal vulnerability (CWE-22) in the Node.js `tmp` package. Versions prior to 0.2.6 pass the caller-supplied `prefix` and `postfix` options directly into the file-system path without sanitizing `../` sequences, allowing an attacker who controls those values to write files outside the designated temporary directory. The fix is to upgrade `tmp` to 0.2.6, which validates and strips path-separator characters from both options before constructing the final path. In this repository, three nested package-lock.json entries pinning `tmp@0.0.28` and `tmp@0.2.7` were removed so the dependency tree resolves to the patched release.

Vulnerability at a Glance

cweCWE-22
fixRemove all nested tmp pins below 0.2.6 from package-lock.json so the dependency tree resolves to the patched release
riskAttacker-controlled prefix or postfix writes files outside /tmp, potentially overwriting sensitive files or planting malicious code
languageJavaScript / Node.js
root causetmp package concatenated caller-supplied strings into a file path without stripping ../ sequences
vulnerabilityPath Traversal via unsanitized tmp prefix/postfix

Introduction

The package-lock.json file in this repository quietly harbored three separate copies of the tmp package — one under node_modules/can-symlink/node_modules/tmp at version 0.0.28, one under node_modules/broccoli/node_modules/tmp at version 0.2.7, and one under node_modules/ember-template-recast/node_modules/tmp also at 0.2.7. Each of those nested pins resolved to a release of tmp that passes caller-supplied prefix and postfix strings directly into a file-system path without stripping ../ sequences — a classic path traversal condition now tracked as CVE-2026-44705 with a HIGH severity rating.

Because tmp is used to create temporary files and directories during build and test tooling (Broccoli, can-symlink, ember-template-recast all depend on it), any code path that lets external input influence the prefix or postfix option flows straight into an unsafe file-path construction routine.


The Vulnerability Explained

What tmp does and where it goes wrong

The tmp package exposes a simple API:

// Typical usage — looks harmless
const tmp = require('tmp');
tmp.file({ prefix: userSuppliedPrefix, postfix: userSuppliedPostfix }, callback);

Internally, vulnerable versions construct the final path by doing something equivalent to:

// Simplified pseudocode from tmp < 0.2.6
const name = prefix + generateRandomChars() + postfix;
const fullPath = path.join(os.tmpdir(), name);

The critical flaw is that prefix and postfix are never sanitized. If an attacker controls either value and injects ../../etc/ as a prefix, the resulting path escapes the temporary directory entirely:

os.tmpdir()  →  /tmp
prefix       →  ../../etc/passwd_
random       →  xK3mZ9
postfix      →  .bak

fullPath     →  /tmp/../../etc/passwd_xK3mZ9.bak
             →  /etc/passwd_xK3mZ9.bak   (after resolution)

Why 0.0.28 is especially dangerous

The can-symlink dependency pinned tmp@0.0.28 — an extremely old release that predates any of the modern input-validation work in the 0.2.x branch. Version 0.0.28 also pulls in os-tmpdir@~1.0.1 as a polyfill, adding another legacy surface area. The engines field in that entry ("node": ">=0.4.0") signals just how ancient this code is:

// Removed from package-lock.json by this fix
"node_modules/can-symlink/node_modules/tmp": {
  "version": "0.0.28",
  "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.28.tgz",
  "dependencies": {
    "os-tmpdir": "~1.0.1"
  },
  "engines": {
    "node": ">=0.4.0"
  }
}

Attack scenario

Consider a build script that accepts a user-defined artifact name and passes it as the prefix to a tmp.dir() call:

// Hypothetical build tooling using the vulnerable tmp
const tmp = require('tmp');
const artifactName = req.body.name; // attacker-controlled

tmp.dir({ prefix: artifactName + '-' }, (err, dirPath) => {
  fs.writeFileSync(path.join(dirPath, 'output.js'), compiledCode);
});

An attacker submits name = "../../home/deploy/.ssh/authorized_keys_". The temporary directory is now created at (or near) /home/deploy/.ssh/, and output.js — potentially containing attacker-chosen content — is written there. On a CI/CD server this could mean planting a malicious script that executes on the next deployment.


The Fix

What changed in package-lock.json

The pull request removes all three nested tmp overrides that were forcing sub-dependency trees to resolve to vulnerable versions:

Removed entry Version Parent package
node_modules/can-symlink/node_modules/tmp 0.0.28 can-symlink
node_modules/broccoli/node_modules/tmp 0.2.7 broccoli
node_modules/ember-template-recast/node_modules/tmp 0.2.7 ember-template-recast

Before (three separate vulnerable pins):

"node_modules/can-symlink/node_modules/tmp": {
  "version": "0.0.28",
  ...
  "dependencies": { "os-tmpdir": "~1.0.1" },
  "engines": { "node": ">=0.4.0" }
},
"node_modules/broccoli/node_modules/tmp": {
  "version": "0.2.7",
  ...
  "engines": { "node": ">=14.14" }
},
"node_modules/ember-template-recast/node_modules/tmp": {
  "version": "0.2.7",
  ...
  "engines": { "node": ">=14.14" }
}

After: all three blocks are deleted, so npm resolves a single hoisted tmp@0.2.6 for the entire dependency tree.

Why 0.2.6 is safe

tmp@0.2.6 introduces explicit sanitization of the prefix and postfix options before they are used in path construction. Path-separator characters and .. sequences are rejected or stripped, so even if an attacker supplies ../../etc/ as a prefix, the library either throws an error or reduces it to a safe string before the path.join() call.

The fix is minimal and non-breaking: valid prefixes (alphanumeric strings, hyphens, underscores) pass through unchanged, so all existing callers in broccoli, can-symlink, and ember-template-recast continue to work exactly as before.


Prevention & Best Practices

1. Never trust caller-supplied path components

Any string that originates outside your own code — HTTP parameters, environment variables, config files, CLI arguments — must be treated as untrusted. Before using such a string in a file path:

const path = require('path');

function safeTmpPrefix(userInput) {
  // Strip everything that isn't alphanumeric, hyphen, or underscore
  const sanitized = userInput.replace(/[^a-zA-Z0-9_-]/g, '');
  if (!sanitized) throw new Error('Invalid prefix');
  return sanitized;
}

2. Validate the resolved path against the expected base

After constructing a path, confirm it still lives inside the intended directory:

const resolvedPath = path.resolve(os.tmpdir(), constructedName);
if (!resolvedPath.startsWith(path.resolve(os.tmpdir()) + path.sep)) {
  throw new Error('Path traversal detected');
}

3. Audit nested dependency pins regularly

The three vulnerable entries existed because sub-dependencies had pinned old tmp versions in their own package-lock.json, and those pins were hoisted into the root lock file. Run npm audit and trivy fs . as part of your CI pipeline to catch these nested overrides before they reach production.

4. Use overrides (npm 8+) or resolutions (Yarn) to force safe versions

If an upstream package won't update quickly, force a safe version at the root:

// package.json
{
  "overrides": {
    "tmp": ">=0.2.6"
  }
}

5. Relevant standards

  • CWE-22: Improper Limitation of a Pathname to a Restricted Directory ("Path Traversal")
  • OWASP: Path Traversal is listed in the OWASP Top 10 under A01:2021 – Broken Access Control
  • OWASP File Upload Cheat Sheet covers safe path handling patterns applicable here

Key Takeaways

  • tmp@0.0.28 (used by can-symlink) is over a decade old and lacks any input validation — if it appears in your lock file, treat it as an immediate risk.
  • Nested package-lock.json pins can silently keep vulnerable versions alive even after you upgrade the top-level dependency; always check npm ls tmp to see every resolved copy.
  • The prefix and postfix options in tmp are a direct path-construction sink — any user-controlled data flowing into them without sanitization is a textbook CWE-22.
  • Removing the nested overrides (not just bumping the top-level version) was the correct fix here, because npm would have continued to hoist the old pinned versions otherwise.
  • Trivy's filesystem scan caught what a simple npm audit might miss — scanner diversity matters for nested-dependency vulnerabilities.

How Orbis AppSec Detected This

  • Source: Caller-supplied prefix and postfix option values passed to tmp.file() / tmp.dir() — values that can be influenced by external input such as build configuration, CLI arguments, or request parameters.
  • Sink: Internal path-join call inside tmp (versions 0.0.28 and 0.2.7) that concatenates the unsanitized prefix/postfix with os.tmpdir() to produce the final file-system path.
  • Missing control: No stripping or rejection of ../, /, or \ characters in the prefix / postfix strings before path construction.
  • CWE: CWE-22 — Improper Limitation of a Pathname to a Restricted Directory (Path Traversal).
  • Fix: Removed all three nested tmp entries (0.0.28 under can-symlink, 0.2.7 under broccoli, 0.2.7 under ember-template-recast) from package-lock.json so the entire dependency tree resolves to the patched tmp@0.2.6.

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.


Conclusion

CVE-2026-44705 is a sharp reminder that dependency hygiene is not a one-time task. Three separate copies of the tmp package — each pinned to a vulnerable version deep inside the node_modules tree — were silently exposing this application to a directory-escape attack. The unsanitized prefix and postfix options in old tmp releases are a direct path from attacker-controlled input to arbitrary file-system writes, a combination that can turn a build tool into a foothold for privilege escalation or code injection on CI/CD infrastructure.

The fix is clean and surgical: remove the nested pins, let npm resolve a single tmp@0.2.6, and the attack surface disappears without changing any application behavior. Pair that with regular npm audit, Trivy filesystem scans, and a root-level overrides policy, and you'll catch the next one before it ships.


References

Frequently Asked Questions

What is a path traversal vulnerability?

A path traversal (CWE-22) vulnerability occurs when user-controlled input containing sequences like `../` is concatenated into a file-system path without sanitization, allowing an attacker to read or write files outside the intended directory.

How do you prevent path traversal in Node.js?

Validate and strip path-separator characters (`/`, `\`, and `..`) from any user-supplied string before using it in a file path. Use `path.basename()` to normalize names, and resolve the final path with `path.resolve()` followed by a check that it still starts with the expected base directory.

What CWE is path traversal?

Path traversal is classified as CWE-22 ("Improper Limitation of a Pathname to a Restricted Directory").

Is using `os.tmpdir()` alone enough to prevent path traversal?

No. Calling `os.tmpdir()` gives you a safe base directory, but if you then concatenate an unsanitized user string as a prefix or postfix, an attacker can still escape that base directory with `../` sequences. The final constructed path must be validated against the base.

Can static analysis detect path traversal in Node.js?

Yes. Tools like Trivy (which flagged this CVE), Semgrep, and Snyk can identify known-vulnerable package versions and taint-flow patterns where unsanitized input reaches path-construction calls.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #168

Related Articles

high

How Path Traversal happens in PostCSS Source Map Auto-Loading and how to fix it

A high-severity path traversal vulnerability (GHSA-r28c-9q8g-f849) in PostCSS versions prior to 8.5.18 allowed attackers to abuse the `sourceMappingURL` comment auto-loading mechanism to read arbitrary `.map` files outside the intended directory. The fix upgrades PostCSS from 8.5.15 to 8.5.18 in `frontend/package-lock.json` and pins the version via an `overrides` block in `frontend/package.json`. This closes a file disclosure primitive that, while not independently exploitable in all configurati

high

How path traversal happens in Python file handling and how to fix it

A path traversal vulnerability in `scripts/merge_m3u.py` allowed user-influenced file paths returned by `glob.glob()` to escape the intended `custom/` directory boundary, potentially exposing arbitrary files on the system. The fix adds a `os.path.realpath()` check that filters out any resolved path that falls outside the expected directory. This is a proactive hardening measure that removes an exploit primitive before it can be chained with other weaknesses.

high

How Path Traversal happens in Node.js scripts and how to fix it

A path traversal vulnerability in `scripts/diff-docx.js` allowed attackers to supply crafted `--output` arguments containing `../` sequences, enabling arbitrary file writes outside the intended working directory. The fix uses `path.resolve()` combined with a working-directory boundary check to ensure all output paths stay within safe bounds. This matters because the script is part of a Node.js library, meaning every downstream consumer was exposed to the same risk.

high

How Path Traversal happens in PostCSS Source Map Loading and how to fix it

A path traversal vulnerability in PostCSS versions before 8.5.18 allowed malicious `sourceMappingURL` comments in CSS files to trick PostCSS into loading arbitrary `.map` files from the filesystem. The fix upgrades PostCSS from 8.5.15 to 8.5.18 in `frontend/package-lock.json` and pins the version via an override in `frontend/package.json`, closing the file disclosure vector before it could be chained with other weaknesses.

critical

How Path Traversal happens in Node.js CLI tools and how to fix it

A path traversal vulnerability in `tools/shot.mjs` allowed attackers to supply a malicious file path as a CLI argument, causing Playwright's `screenshot()` method to write files to arbitrary filesystem locations — including sensitive system directories. The fix introduces a new `safepath.mjs` module that resolves and validates every output path against the project root before any file is written.

high

How Missing pnpm Trust Policy and Release Age Settings Happen in Node.js Workspaces and How to Fix Them

A pnpm workspace configuration was missing two critical security hardening settings — `trustPolicy` and `minimumReleaseAge` — leaving the project vulnerable to malicious package updates and newly published, potentially compromised package versions. The fix adds `trustPolicy: no-downgrade`, `minimumReleaseAge: 10080`, and `blockExoticSubdeps: true` to `pnpm-workspace.yaml`, raising the security bar against supply chain attacks. These settings, available since pnpm v10.16.0 and v10.21.0 respective