Back to Blog
critical SEVERITY5 min read

How Path Traversal Vulnerabilities Happen in Node.js Development Servers and How to Fix Them

A critical path traversal vulnerability was discovered in the development file server script `serve.mjs`, where arbitrary directory paths from command-line arguments were accepted without validation. This flaw could allow attackers to serve any directory on the filesystem over HTTP, potentially exposing sensitive system files like `/etc/passwd` or application secrets. The fix adds a simple but effective validation check ensuring the serve root stays within the current working directory.

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

Answer Summary

Path traversal (CWE-22) in Node.js occurs when file paths from untrusted input aren't validated against a safe base directory. In `serve.mjs`, the `process.argv[2]` argument was passed directly to `resolve()` without checking if the resulting path escaped the project root. The fix validates that the resolved path starts with `process.cwd()`, preventing directory traversal attacks.

Vulnerability at a Glance

cweCWE-22
fixValidate that resolved path starts with current working directory
riskFilesystem exposure via HTTP, sensitive data leakage
languageJavaScript (Node.js)
root causeCommand-line directory argument accepted without boundary validation
vulnerabilityPath Traversal / Arbitrary Directory Serving

Introduction

The scripts/serve.mjs file in this project implements a lightweight development file server, designed to serve static files during local development. However, a critical flaw on line 5 created a dangerous security gap: the script accepted any directory path from process.argv[2] and passed it directly to resolve() without validating whether that path stayed within the project boundaries.

Here's the vulnerable code pattern:

const root = resolve(process.argv[2] ?? '.');

This single line, while seemingly innocuous, allowed anyone who could influence the command-line arguments—whether through a compromised build script, malicious CI/CD configuration, or social engineering—to serve arbitrary directories from the host filesystem over HTTP.

The Vulnerability Explained

What Made This Code Dangerous

The resolve() function from Node.js's path module converts any path to an absolute path. When combined with unvalidated user input from process.argv[2], it becomes a gateway to the entire filesystem.

Consider what happens when an attacker modifies the invocation:

node scripts/serve.mjs /etc

The server would happily serve the contents of /etc over HTTP on port 4173. An attacker could then access:

  • /etc/passwd - user account information
  • /etc/shadow (if permissions allow) - password hashes
  • Application configuration files with database credentials
  • SSH keys from home directories
  • Environment files containing API tokens

The Attack Chain

This vulnerability requires a 2-step exploitation chain:

  1. Gain influence over the command-line arguments: This could happen through:
    - Compromising the CI/CD pipeline configuration
    - Modifying build scripts in a malicious PR
    - Social engineering a developer to run a modified command

  2. Access the exposed files: Once the server starts with a malicious root directory, the attacker accesses sensitive files via HTTP requests to http://localhost:4173/passwd or similar paths.

Real-World Impact

In a development environment, this vulnerability is particularly dangerous because:

  • Development machines often have broader filesystem access than production servers
  • Developers may have SSH keys, cloud credentials, and API tokens accessible
  • CI/CD runners might expose secrets mounted as files
  • The development server likely runs without authentication

The Fix

The fix adds a critical validation check immediately after resolving the path:

Before (Vulnerable)

import { createServer } from 'node:http';
import { readFile, stat } from 'node:fs/promises';
import { extname, join, normalize, resolve } from 'node:path';

const root = resolve(process.argv[2] ?? '.');
const port = Number(process.env.PORT ?? 4173);

After (Secure)

import { createServer } from 'node:http';
import { readFile, stat } from 'node:fs/promises';
import { extname, join, normalize, resolve } from 'node:path';

const root = resolve(process.argv[2] ?? '.');
if (!root.startsWith(process.cwd())) {
  console.error(`Error: serve root "${root}" is outside the current working directory.`);
  process.exit(1);
}
const port = Number(process.env.PORT ?? 4173);

Why This Fix Works

The fix implements a directory boundary check using startsWith():

  1. process.cwd() returns the current working directory—the directory from which the script was invoked
  2. root.startsWith(process.cwd()) ensures the resolved serve root is either the current directory or a subdirectory within it
  3. If the check fails, the script exits immediately with a clear error message, preventing the server from starting

This approach follows the principle of allowlisting: instead of trying to block malicious patterns (which attackers can often bypass), it explicitly defines what's allowed and rejects everything else.

Attempting to Exploit the Fixed Code

# Attacker tries to serve /etc
$ node scripts/serve.mjs /etc
Error: serve root "/etc" is outside the current working directory.

# Attacker tries path traversal
$ node scripts/serve.mjs ../../../etc
Error: serve root "/etc" is outside the current working directory.

# Legitimate use still works
$ node scripts/serve.mjs ./dist
# Server starts normally, serving ./dist

Key Takeaways

  • Never trust process.argv for filesystem paths without validating boundaries against a known-safe root directory
  • The resolve() function doesn't provide security—it only normalizes paths; boundary validation is your responsibility
  • Development tools need security too—attackers often target build systems and development infrastructure
  • A 4-line fix prevented potential full filesystem exposure—simple validation checks have outsized security impact
  • startsWith(process.cwd()) is an effective boundary check for development servers that should only serve project files

How Orbis AppSec Detected This

  • Source: Command-line argument process.argv[2] in scripts/serve.mjs:5
  • Sink: resolve() function call used to set the HTTP server's file serving root
  • Missing control: No validation that the resolved path stays within the project directory
  • CWE: CWE-22 (Improper Limitation of a Pathname to a Restricted Directory)
  • Fix: Added a startsWith(process.cwd()) check that terminates the process if the serve root escapes the current working directory

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

This path traversal vulnerability in serve.mjs demonstrates how even simple development utilities can introduce critical security risks. The unvalidated process.argv[2] argument allowed potential attackers to expose any directory on the filesystem through the development server.

The fix—a simple four-line boundary check—shows that effective security doesn't always require complex solutions. By validating that the serve root stays within process.cwd(), the script now safely rejects any attempt to serve directories outside the project.

When building development tools, remember: convenience should never come at the cost of security. Always validate paths, always assume inputs are malicious, and always implement the simplest effective control.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #4

Related Articles

high

How Denial of Service via Infinite Loop Happens in JavaScript Dependencies and How to Fix It

CVE-2026-67213 is a high-severity denial of service vulnerability in nanoid before version 5.1.6 that triggers an infinite loop during random ID generation when processing specially crafted input. We upgraded nanoid across the entire dependency tree to patch this flaw and prevent attackers from freezing application threads. This fix ensures that ID generation remains resilient even when handling adversarial input patterns.

high

How Sensitive Data Exposure happens in Zotero plugins and how to fix it

A high-severity data exposure vulnerability in `Zotero.ts` automatically transmitted complete document metadata—including private notes, attachment paths, and tags—to external LLM services without user consent. The fix replaces broad `item.toJSON()` serialization with explicit field selection, sending only essential bibliographic data.

high

How missing dependency update cooldowns happen in GitHub Dependabot configurations and how to fix it

A semgrep scan flagged `.github/dependabot.yml` for lacking a cooldown period, meaning Dependabot would immediately propose updates to brand-new package versions across npm, Bundler, and Docker ecosystems. The fix adds a `cooldown: default-days: 7` block to every `package-ecosystem` entry, forcing a one-week waiting period before newly published releases are considered — reducing exposure to malicious or unstable package drops.

high

How Path Traversal Happens in TensorFlow's Data Service and How to Fix It

TensorFlow's data service dispatcher validated dataset IDs against forward-slash traversal attacks but overlooked backslash characters on non-Windows platforms, allowing attackers to escape the root directory. A targeted fix adds explicit backslash validation across all platforms, closing a high-severity path traversal vulnerability in the snapshot management system.

critical

How Unbounded WebSocket Message Handling Causes Resource Exhaustion in Node.js and How to Fix It

The WebSocketCrossServerAdapter class in a popular Node.js WebSocket library lacked any rate limiting on inbound messages, allowing attackers to flood Redis nodes and WebSocket servers with high-volume traffic. The fix introduces a configurable `rateLimit` option that caps messages per connection per second, preventing resource exhaustion while preserving legitimate functionality.

critical

How Remote Code Execution Happens in Handlebars Template Compilation and How to Fix It

CVE-2026-33937 is a critical remote code execution vulnerability in Handlebars.js that allows attackers to execute arbitrary code by passing maliciously crafted Abstract Syntax Tree (AST) objects to the compile() function. The vulnerability was patched in version 4.7.9, and we've upgraded to protect against this threat vector.