The Vulnerability Explained
The readSourceFile function is responsible for fetching source files during model export. It takes two parameters: dirName (a base directory) and relPath (a relative path within that directory), concatenates them, and passes the result to the browser's fetch() API to retrieve the file.
Before the fix, this concatenation happened without any validation:
async function readSourceFile(dirName, relPath) {
const rawUrl = `${normalizeDir(dirName)}${relPath}`;
const web = isWebDir(dirName);
try {
const res = await fetch(web ? rawUrl : convertFileSrc(rawUrl));
An attacker who can control either dirName or relPath can inject path traversal sequences. For example:
- If dirName is /app/models/ and relPath is ../../../etc/passwd, the resulting URL would be /app/models/../../../etc/passwd, which resolves to /etc/passwd.
- If relPath uses the file:// protocol prefix or absolute paths, the attacker could bypass the directory restriction entirely.
The threat is especially acute if either parameter comes from user input—HTTP request parameters, file upload metadata, or configuration files. The fetch() call then reads whatever file the constructed URL resolves to, returning its contents to the attacker.
Real-World Impact
In a web application context, this could expose:
- Configuration files containing database credentials or API keys
- Source code files revealing application logic or other vulnerabilities
- System files like /etc/passwd (on Unix systems)
- Private key files if the process runs with sufficient file permissions
- Application secrets stored in environment files or .env files
Affected Versions
| Affected | not applicable (first-party code) |
| Fixed in | not applicable (first-party code) — see PR for commit context |
| Ecosystem | N/A |
| CVE / GHSA | not assigned |
| CWE | CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') |
The Fix
The fix introduces a new safeRelPath() function that validates and normalizes relative paths before they're concatenated into URLs:
function safeRelPath(relPath) {
const parts = String(relPath).replace(/\\/g, '/').split('/');
const stack = [];
for (const part of parts) {
if (part === '' || part === '.') continue;
if (part === '..') {
if (stack.length === 0) return null;
stack.pop();
} else {
stack.push(part);
}
}
return stack.join('/');
}
async function readSourceFile(dirName, relPath) {
const safePath = safeRelPath(relPath);
if (safePath === null) return null;
const rawUrl = `${normalizeDir(dirName)}${safePath}`;
How this works:
- Normalize slashes: Backslashes are converted to forward slashes, preventing Windows path tricks.
- Split and iterate: The path is split into components and processed one by one.
- Skip empty and dot: Empty strings (from double slashes) and
.(current directory) are ignored. - Validate
..sequences: When a..is encountered, it pops the last component from the stack—but only if the stack is not empty. If..appears when the stack is empty (meaning the attacker tried to escape the root), the function returnsnull, signaling an invalid path. - Reconstruct and return: The remaining components are joined back into a safe path.
Examples:
- Input:
models/subdir/file.js→ Output:models/subdir/file.js(unchanged) - Input:
../../../etc/passwd→ Output:null(too many..escapes) - Input:
models/../file.js→ Output:file.js(valid traversal back to root) - Input:
models/./subdir/file.js→ Output:models/subdir/file.js(dot removed)
The readSourceFile function now checks if safeRelPath() returned null and aborts the fetch, preventing the attack.
How Orbis AppSec Detected This
Source: The relPath parameter in the readSourceFile() function (tainted data entry point).
Sink: The fetch() call receiving the concatenated rawUrl (dangerous operation).
Missing control: No validation or normalization of the relPath parameter before URL construction. The code assumed that concatenating two strings would stay within the intended directory boundary.
CWE: CWE-22 — Improper Limitation of a Pathname to a Restricted Directory.
Fix: A path normalization function that resolves .. sequences, rejects escape attempts, and validates the result before the URL is constructed.
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.
Key Takeaways
- Never concatenate user input directly into file paths or URLs. Always normalize and validate relative paths using a function that rejects unbalanced
..sequences (likesafeRelPath()here). - Path traversal is easy to miss. Simple string concatenation feels safe but isn't—attackers can use
..,./, backslashes, and protocol handlers to escape intended boundaries. - Test path normalization with adversarial inputs. The fix should reject
../../,../../../etc/passwd,..\\..\\windows\\system32, and mixed separators before they reach dangerous APIs. - Validate at the entry point. If
readSourceFile()is called from untrusted sources (HTTP parameters, file uploads, external APIs), validate inputs immediately at the function boundary, not deeper in the call stack. - URL normalization differs from filesystem normalization. Relative path handling in
fetch()can differ from OS file APIs; always test against the actual API (browser fetch, Node.js file system, etc.) to understand its resolution behavior.
Conclusion
The path traversal in modelExporter.js showed how a straightforward string concatenation—${dirName}${relPath}—can become a critical vulnerability when either parameter comes from untrusted input. The fix's stack-based path resolver ensures that .. sequences can't escape the intended directory, and that invalid traversal attempts are rejected outright. This pattern is reusable: whenever you build file paths or URLs from user-controlled components, apply the same normalization logic before the path reaches a filesystem or fetch operation.