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
•Technically reviewed by Anupam Mediratta•Published August 26, 2026•Reviewed 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.


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.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #168

Related Articles

high

clean_path() Path Traversal: rel_path Escapes USERDATA Dir

A path traversal flaw in the `clean_path()` helper let a user-controlled `rel_path` value escape the intended USERDATA directory and reach arbitrary files on disk. The function joined path segments without checking the final result, so `../` sequences in route parameters or query strings could be used to read or write files outside the sandboxed storage area. The fix normalizes the path and verifies it still resolves inside the USERDATA root before returning it, raising an error otherwise.

critical

path.resolve() Path Traversal in Node CLI's Dynamic import()

A Node.js CLI script for validating expression definitions took a file path from `process.argv[2]`, resolved it with `path.resolve()`, and passed the result straight into a dynamic `import()` — with no check that the resolved path stayed inside the working directory. An attacker (or a malicious skill/plugin invocation) could supply traversal sequences to load and execute arbitrary `.js` files from anywhere on disk. The fix adds a boundary check with `path.relative()` and an extension allowlist b

high

bookDir() Path Traversal via Unsanitized bookId Parameter

The `bookDir()` function accepted unsanitized `bookId` values derived from user-created book titles, enabling path traversal attacks through `../` sequences. A fix was applied that validates the identifier using `path.basename()` and throws on mismatch, ensuring all resolved paths remain within `LIBRARY_DIR`.

high

Express `app.get('*')` Wildcard Handler Path Traversal in watch.js

A first-party Express server's wildcard route handler used `req.url.indexOf('font.woff2')` to gate access to a font file, allowing attackers to bypass the substring check with crafted paths. The fix replaces the catch-all handler with explicit route registration.

high

updateCardBg() Follows Unvalidated 302 Location Headers

A background-image updater fetched a configured image URL with manual redirect handling and then re-issued the request to whatever `Location` header came back, with no scheme or host checks. A redirect to `http://169.254.169.254/` or `http://127.0.0.1:<port>/` would have been followed with the original fetch options attached, and the response body written to disk as an image asset. The fix resolves the redirect target against `imgDownloadUrl` and rejects anything that is not HTTPS on the same ho

high

markitdown_bridge.py Path Traversal: Arbitrary File Read via sys.argv

The markitdown_bridge.py script, used by MDView for DOCX-to-Markdown conversion, accepted file paths directly from command-line arguments without validating they stayed within intended directories. An attacker could exploit this to read arbitrary files from the filesystem by passing path traversal sequences in the source_path parameter.