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, 2026•Reviewed 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

linkify-it 5.0.1 mailto: Link Parsing Causes DoS

linkify-it versions up to 5.0.1 can be forced into excessive processing time when autolinking a specially crafted mailto: link, allowing a remote attacker to degrade or stall the parsing thread. Upgrading to linkify-it 5.0.2 closes the issue; any application that runs linkify-it (directly or via markdown-it) against untrusted text should update immediately.

critical

Slim CLI Unverified Remote Fetch in Version Check

The Slim CLI's version check command fetched remote package metadata without integrity verification, enabling attackers to serve malicious responses through repository hijacking or man-in-the-middle attacks. The fix adds input validation, HTTP status checking, and response schema verification to ensure only legitimate version data is processed.

critical

ensureTrivy() CWE-494: Unverified Trivy Binary Download

The `ensureTrivy()` function fetched the Trivy vulnerability scanner from GitHub releases without verifying its integrity, exposing applications to supply-chain attacks. An attacker in a MITM position or a compromised CDN could substitute a malicious binary that executes with the application's privileges. The fix adds cryptographic verification using SHA256 checksums published alongside each release.

high

@xmldom/xmldom 0.9.10 ReDoS: CVE-2026-83606 in PI Parsing

The dependency tree pinned `@xmldom/xmldom` at 0.9.10, a version vulnerable to CVE-2026-83606: a regular expression in the processing-instruction (`<?target data?>`) parsing path backtracks catastrophically on crafted input, so a single XML document can consume CPU for seconds or minutes. Because Node.js runs application code on one thread, that stall blocks every other request in flight. The lockfile was updated to resolve `@xmldom/xmldom` 0.9.12, which carries the fix.

high

Unbounded Map in createLoginRateLimiter Exhausts API Memory

The console API's `createLoginRateLimiter` and `createMutationRateLimiter` stored one `Map` entry per client key with no upper bound and no expiry sweep, so an attacker rotating source addresses or identifiers could grow those maps until the Node process hit an out-of-memory crash. The fix introduces a `maxTrackedKeys` option (default 5000), a `trackedEntryLimit` sanitizer, and an `evictOldestIfFull` helper that drops the oldest tracked key while protecting the shared global counter. The rate li

high

js-yaml 4.3.1 Denial of Service: Malformed Input Hangs YAML Parser

A denial of service vulnerability in js-yaml versions 4.3.1 and earlier allows attackers to hang the YAML parser indefinitely by providing specially crafted malformed input. The fix, released in versions 4.3.2 and 3.15.2, patches the parsing logic to prevent unbounded processing. Upgrading is recommended for all applications parsing untrusted YAML data.