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 bycan-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 tmpto see every resolved copy. - The
prefixandpostfixoptions intmpare 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 auditmight miss — scanner diversity matters for nested-dependency vulnerabilities.
How Orbis AppSec Detected This
- Source: Caller-supplied
prefixandpostfixoption values passed totmp.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(versions0.0.28and0.2.7) that concatenates the unsanitized prefix/postfix withos.tmpdir()to produce the final file-system path. - Missing control: No stripping or rejection of
../,/, or\characters in theprefix/postfixstrings before path construction. - CWE: CWE-22 — Improper Limitation of a Pathname to a Restricted Directory (Path Traversal).
- Fix: Removed all three nested
tmpentries (0.0.28undercan-symlink,0.2.7underbroccoli,0.2.7underember-template-recast) frompackage-lock.jsonso the entire dependency tree resolves to the patchedtmp@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.