Back to Blog
critical SEVERITY7 min read

How Path Traversal and Resource Exhaustion happen in Node.js HTTP servers and how to fix them

A critical security vulnerability in `wasm-build/server.js` allowed attackers to read arbitrary files outside the web root via path traversal, while simultaneously leaving the server open to resource exhaustion through unbounded concurrent connections. The fix sanitizes URL paths before joining them to the filesystem and enforces strict connection and timeout limits to prevent denial-of-service attacks.

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

Answer Summary

This vulnerability combines a path traversal flaw (CWE-22) and a resource exhaustion/DoS risk (CWE-400) in a Node.js HTTP server (`wasm-build/server.js`). The path traversal occurred because `req.url` was joined directly to `__dirname` without sanitizing `../` sequences, letting attackers read files outside the web root. The DoS risk arose from no connection limits or timeouts. The fix normalizes and validates the resolved path against `__dirname`, strips traversal segments, and sets `maxConnections`, `timeout`, `headersTimeout`, and `requestTimeout` on the server instance.

Vulnerability at a Glance

cweCWE-22 (Path Traversal), CWE-400 (Uncontrolled Resource Consumption)
fixNormalize and boundary-check resolved paths; enforce `maxConnections` and timeout settings
riskArbitrary file read outside web root; server crash via connection flooding
languageJavaScript (Node.js)
root cause`req.url` joined to `__dirname` without sanitizing traversal sequences; no connection or timeout limits
vulnerabilityPath Traversal + Resource Exhaustion (DoS)

The Vulnerability in Context

The wasm-build/server.js file is a lightweight Node.js HTTP server that serves static files for a WebAssembly demo. It reads a URL from an incoming request, maps it to a file on disk, and streams the file back to the client. Simple enough—but two distinct security flaws were hiding in that straightforward logic:

  1. Path traversal: User-controlled URL input was joined directly to the server's base directory without sanitizing ../ sequences, allowing an attacker to escape the web root and read arbitrary files on the host.
  2. Resource exhaustion: No connection limits, timeouts, or request-size constraints existed, leaving the server open to denial-of-service attacks from connection flooding.

Together, these issues could let an attacker exfiltrate sensitive files from the host machine and take the server offline. Because this file ships in a Node.js library, both risks cascade to every downstream consumer of the package.


The Vulnerability Explained

Path Traversal (CWE-22)

Here is the original code at the heart of the problem (around line 23 in server.js):

// BEFORE — vulnerable
let filePath = req.url === '/' ? '/demo.html' : req.url;
filePath = path.join(__dirname, filePath);

req.url is raw, attacker-controlled input. path.join() does resolve .. segments—but it resolves them relative to the joined result, which means a crafted URL can still produce a path that escapes __dirname.

Concrete attack scenario:

GET /../../../../etc/passwd HTTP/1.1
Host: localhost:8000

path.join('/app/wasm-build', '/../../../../etc/passwd') resolves to /etc/passwd on Linux. The server then calls fs.access() and streams the file back. No authentication, no boundary check, no error—just the raw contents of /etc/passwd (or any other file the Node.js process can read).

On a developer machine or CI environment, this could expose SSH keys (~/.ssh/id_rsa), environment files (.env), cloud credentials, or application secrets.

Resource Exhaustion / DoS (CWE-400)

The original http.createServer() call set no limits at all:

// BEFORE — no limits
const server = http.createServer((req, res) => { ... });
server.listen(PORT, () => { ... });

Node.js defaults leave maxConnections unbounded and timeouts at several minutes. An attacker running:

ab -n 100000 -c 500 http://target:8000/

…can exhaust the server's file descriptors, memory, and CPU within seconds. Slow-loris style attacks—opening many connections and drip-feeding headers—are equally effective because headersTimeout was never set.


The Fix

The pull request makes two targeted changes to wasm-build/server.js.

Fix 1: Path Sanitization and Boundary Enforcement

// AFTER — safe
let urlPath = req.url === '/' ? '/demo.html' : req.url;
// Sanitize the requested path before use: strip any traversal segments
urlPath = path.normalize(urlPath).replace(/^(\.\.[\/\\])+/, '');
let filePath = path.join(__dirname, urlPath);

// Prevent path traversal outside of __dirname
if (!filePath.startsWith(__dirname)) {
    res.writeHead(403, { 'Content-Type': 'text/html' });
    res.end('<h1>403 - Forbidden</h1>');
    return;
}

Three things happen here:

Step What it does Why it matters
path.normalize(urlPath) Resolves . and .. segments in the URL path Canonicalizes the path before any further processing
.replace(/^(\.\.[\/\\])+/, '') Strips any leading ../ or ..\ sequences Removes traversal attempts at the start of the normalized path
filePath.startsWith(__dirname) Asserts the final resolved path is inside the web root Defense-in-depth: catches any edge case the regex misses

The startsWith(__dirname) check is the critical defense-in-depth layer. Even if a creative encoding or platform quirk slips past the regex, the boundary assertion will catch it and return a 403 Forbidden response.

Fix 2: Connection and Timeout Limits

// AFTER — resource limits enforced
server.maxConnections = 100;
server.timeout = 30000;
server.headersTimeout = 10000;
server.requestTimeout = 10000;
Setting Value Effect
maxConnections 100 Caps simultaneous open sockets; new connections are queued or refused beyond this
timeout 30 000 ms Closes idle keep-alive sockets after 30 seconds
headersTimeout 10 000 ms Aborts connections that don't complete headers within 10 seconds (mitigates slow-loris)
requestTimeout 10 000 ms Aborts requests that take longer than 10 seconds to complete

These four lines transform an unlimited server into one with predictable resource consumption under adversarial load.


Key Takeaways

  • path.join(__dirname, req.url) is not safe by itself. You must also call path.normalize(), strip leading traversal sequences, and assert the result starts with your base directory—all three steps together.
  • The startsWith(__dirname) boundary check is the last line of defense. Even if path normalization and regex stripping are bypassed by an unexpected encoding, this assertion catches the escape attempt.
  • Node.js HTTP servers are unbounded by default. Without explicit maxConnections, headersTimeout, and requestTimeout, any production server is trivially vulnerable to resource exhaustion.
  • This demo server shipped in a library package. "Demo" or "build tool" code that gets distributed still needs to be hardened—downstream consumers may run it in environments you don't control.
  • Four lines of configuration (server.maxConnections, server.timeout, server.headersTimeout, server.requestTimeout) eliminated the DoS surface entirely. Defense doesn't always require architectural changes.

How Orbis AppSec Detected This

  • Source: req.url — raw, attacker-controlled input from every incoming HTTP request
  • Sink: path.join(__dirname, filePath) followed by fs.access(filePath, ...) in wasm-build/server.js:23, where the unvalidated path is used for filesystem access
  • Missing control: No path normalization, no traversal-sequence stripping, and no boundary assertion that the resolved path remains inside __dirname; additionally, no maxConnections, headersTimeout, or requestTimeout on the server instance
  • CWE: CWE-22 (Improper Limitation of a Pathname to a Restricted Directory) and CWE-400 (Uncontrolled Resource Consumption)
  • Fix: The path is now normalized and stripped of traversal sequences before joining, a startsWith(__dirname) guard returns 403 on escape attempts, and four server-level resource limits cap connection and request duration

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

Two vulnerabilities in roughly 10 lines of code in wasm-build/server.js combined to create a serious risk: arbitrary file read via path traversal and server crash via connection flooding. The path traversal fix demonstrates a pattern every Node.js developer should internalize—path.join() is not a sanitizer, and user-controlled paths must always be validated against a boundary after resolution. The resource exhaustion fix is a reminder that Node.js ships with no connection limits by default, and that four lines of configuration can make the difference between a resilient server and one that falls over under trivial load.

Security in static file servers is easy to overlook because the logic feels simple. As this case shows, simple logic and serious vulnerabilities are not mutually exclusive.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #153

Related Articles

high

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.

critical

How path traversal happens in PHP virtual filesystem adapters and how to fix it

A critical path traversal flaw in `VirtualAdapter.php`'s `resolveMount()` method allowed attackers to escape mounted directory boundaries using sequences like `../../../etc/passwd`. The fix introduces `PathPolicy::normalizeRelative()` to sanitize the remaining path segment before it ever reaches the underlying storage adapter.

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.

high

How Trust-Prefix Bypass via Path Traversal Happens in Python Copier and How to Fix It

CVE-2026-53951 is a high-severity path traversal vulnerability in Copier 9.15.0 that allowed attackers to bypass trust-prefix checks and execute tasks without user confirmation. Upgrading to Copier 9.15.2 eliminates this attack vector by properly validating file paths before task execution.

critical

How Path Traversal in basic-ftp Leads to File Overwrite Attacks and How to Fix It

CVE-2026-27699 is a critical path traversal vulnerability in basic-ftp versions before 5.3.1 that allows attackers to overwrite arbitrary files on the system by crafting malicious file paths. This vulnerability was fixed by upgrading basic-ftp and enforcing strict version constraints across dependent packages. Understanding this attack and its mitigation is essential for developers using FTP libraries in production environments.

critical

How Command Injection Vulnerabilities Happen in Python Subprocess Calls and How to Fix Them

A critical command injection vulnerability was discovered in `src/unused/server/fft.py` where external binaries like `oggenc` and `cocoa_text` were executed with file path parameters that could be manipulated by user input. Although `shell=False` was used, the lack of input validation allowed attackers to potentially trigger processing of arbitrary files or cause denial of service. This fix implements proper path validation to prevent exploitation.