Back to Blog
high SEVERITY4 min read

modelExporter.js Path Traversal via Unsanitized Directory Concatenation

A path traversal vulnerability in `modelExporter.js` allowed attackers to read arbitrary files by injecting traversal sequences into directory and relative path parameters. The `readSourceFile` function concatenated these unsanitized inputs directly into file URLs passed to `fetch()`. The fix introduces strict path normalization that rejects attempts to escape the intended directory.

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published September 12, 2026•Reviewed September 12, 2026

Answer Summary

The `readSourceFile` function in modelExporter.js concatenates user-controlled `dirName` and `relPath` parameters directly into file URLs without validation. An attacker controlling these parameters can inject path traversal sequences like `../../../etc/passwd` to read arbitrary files from the filesystem. The fix adds a `safeRelPath()` function that normalizes the path, rejects `..` sequences that attempt to escape the root, and validates the result before URL construction. CWE-22 (Improper Limitation of a Pathname to a Restricted Directory).

Vulnerability at a Glance

cweCWE-22
fixPath normalization function that resolves `..` sequences and rejects escape attempts
riskUnauthenticated attackers can read arbitrary files accessible to the application process
languageJavaScript
root causeDirect concatenation of user-controlled path components without normalization or validation
vulnerabilityPath Traversal / Directory Traversal

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:

  1. Normalize slashes: Backslashes are converted to forward slashes, preventing Windows path tricks.
  2. Split and iterate: The path is split into components and processed one by one.
  3. Skip empty and dot: Empty strings (from double slashes) and . (current directory) are ignored.
  4. 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 returns null, signaling an invalid path.
  5. 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 (like safeRelPath() 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.

Prevention and further reading

Frequently Asked Questions

What happens if an attacker passes `../../../etc/passwd` as the `relPath` parameter to `readSourceFile()`?

Before the fix, this would be concatenated directly into the file URL, allowing the fetch to read `/etc/passwd` if the `dirName` didn't contain sufficient path depth. After the fix, `safeRelPath()` detects the unbalanced `..` and returns `null`, preventing the read.

Does the fix prevent attacks that use URL encoding or backslash separators in the path?

Yes. The `safeRelPath()` function normalizes backslashes to forward slashes and processes the path component-by-component, so encoded traversal sequences like `%2e%2e%2fpasswd` are normalized before the `..` check runs.

Can an attacker still read files if they know the absolute path and control the `dirName` parameter?

The fix prevents relative traversal, but if an attacker can influence `dirName` itself (not just `relPath`), they may still construct arbitrary URLs. The vulnerability scope depends on which callers of `readSourceFile()` receive untrusted input for `dirName`.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #30

Related Articles

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.

high

SHACL Viewer Path Traversal in graph3d(): Unvalidated `path`

The `graph3d()` and `graph2d()` request handlers in SHACL Viewer directly concatenated user-supplied `path` parameters into filesystem paths, enabling directory traversal outside the intended `/shapes/` directory. The fix introduces `_resolve_shapes_path()` with `os.path.realpath()` validation to enforce containment within the shapes directory.