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.


Prevention & Best Practices

For Path Traversal

  1. Always validate after joining. path.join() is not a security function—it's a convenience function. After joining user input to a base directory, always assert resolvedPath.startsWith(baseDir).

  2. Use an allowlist of valid paths when possible. If the server only needs to serve a known set of files, enumerate them explicitly rather than mapping arbitrary URL paths to the filesystem.

  3. Run the process with least privilege. If the Node.js process only needs to read files inside wasm-build/, run it as a user with no access to the rest of the filesystem. Path traversal becomes much less dangerous when the process can't read /etc/passwd in the first place.

  4. Use a dedicated static file server. Libraries like serve-static or frameworks like Express have path traversal protections built in and battle-tested. Rolling your own file server introduces risk.

For Resource Exhaustion

  1. Set server.maxConnections for every Node.js HTTP server. There is no sensible default that protects you—you must set it explicitly.

  2. Always configure headersTimeout and requestTimeout. Slow-loris attacks are trivially easy to execute and devastatingly effective against servers with no header timeout.

  3. Add a reverse proxy (nginx, Caddy) in front of Node.js servers. Reverse proxies handle connection limiting, TLS termination, and request buffering before traffic reaches your application code.

  4. Implement rate limiting at the application layer. Libraries like express-rate-limit or middleware-level solutions add per-IP throttling on top of the server-level limits.

Relevant Standards

  • OWASP Top 10 A01:2021 – Broken Access Control covers path traversal as a primary attack vector.
  • OWASP Path Traversal Cheat Sheet provides language-specific guidance.
  • CWE-22: Improper Limitation of a Pathname to a Restricted Directory
  • CWE-400: Uncontrolled Resource Consumption

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.


References

Frequently Asked Questions

What is path traversal?

Path traversal (CWE-22) is a vulnerability where an attacker manipulates file path inputs—using sequences like `../`—to access files and directories outside the intended directory.

How do you prevent path traversal in Node.js?

Normalize the path with `path.normalize()`, strip leading traversal segments, join with the base directory using `path.join()`, and then assert the resolved path still starts with the intended base directory before using it.

What CWE is path traversal?

Path traversal is CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

Is `path.join()` alone enough to prevent path traversal in Node.js?

No. `path.join(__dirname, req.url)` resolves `../` sequences but does not prevent the result from escaping `__dirname`. You must also assert that the resolved path starts with `__dirname` after joining.

Can static analysis detect path traversal?

Yes. Tools like Semgrep, CodeQL, and multi-agent AI scanners can trace tainted data from HTTP request parameters to filesystem calls and flag missing boundary checks, exactly as Orbis AppSec did here.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #153

Related Articles

high

How command injection happens in Node.js child_process spawn calls and how to fix it

A benchmarking helper in `bench/lib/actor.js` passed an unvalidated executable path from upstream pipeline results directly into `child_process.spawn()`. The fix resolves the path and enforces that it lives inside the sandboxed stage directory before execution, closing off a path-traversal-driven command injection primitive.

high

How path traversal happens in Python and how to fix it

A high-severity path traversal vulnerability in `posttrain_runner.py` allowed arbitrary file reads through the `base_ckpt` parameter. The fix implements `os.path.realpath()` validation to ensure all file paths remain within the working directory, preventing attackers from accessing sensitive system files.

high

How Path Traversal happens in Node.js tmp package and how to fix it

The tmp package version 0.0.33 contained a high-severity path traversal vulnerability (CVE-2026-44705) that allowed attackers to escape temporary directories through unsanitized prefix and postfix parameters. This reddit-app project was upgraded from tmp 0.0.33 to 0.2.7, which implements proper input sanitization to prevent directory traversal attacks and removes the deprecated os-tmpdir dependency.

medium

How Path Traversal and Filename Injection Happens in Python File Handling and How to Fix It

A medium-severity path traversal vulnerability in `PainterNode/painter_node.py` allowed attackers to reference files outside the intended directory by exploiting a broken `isFileName()` validation function. The original logic used incorrect boolean operators, meaning the filename guard never actually blocked malicious inputs like `../../../etc/passwd` or paths containing backslashes. The fix rewrites the condition with proper logic and adds explicit checks for path separator characters and direc

critical

How Archive Path Traversal Happens in Node.js and How to Fix It

CVE-2026-53486 is a critical path traversal vulnerability in the Decompress library, where crafted archive entries can write files and symbolic links outside the intended extraction directory. This vulnerability was transitively introduced through `@vitest/browser` and related packages pinned at version 4.1.5, and was resolved by upgrading to 4.1.6 and 5.0.0-beta.3. Left unpatched, an attacker who controls an archive file processed by any downstream consumer of this dependency chain could overwr

critical

How Cross-Site Scripting happens in fast-xml-parser and how to fix it

CVE-2026-25896 is a critical Cross-Site Scripting vulnerability in fast-xml-parser stemming from improper DOCTYPE entity handling, which could allow attackers to inject malicious scripts through crafted XML payloads. The fix upgrades the vulnerable dependency from version 4.4.1 to patched versions 5.3.5 and 4.5.4, eliminating the unsafe parsing behavior while preserving all legitimate XML processing functionality.