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:
- 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. - 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
-
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 assertresolvedPath.startsWith(baseDir). -
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.
-
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/passwdin the first place. -
Use a dedicated static file server. Libraries like
serve-staticor frameworks like Express have path traversal protections built in and battle-tested. Rolling your own file server introduces risk.
For Resource Exhaustion
-
Set
server.maxConnectionsfor every Node.js HTTP server. There is no sensible default that protects you—you must set it explicitly. -
Always configure
headersTimeoutandrequestTimeout. Slow-loris attacks are trivially easy to execute and devastatingly effective against servers with no header timeout. -
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.
-
Implement rate limiting at the application layer. Libraries like
express-rate-limitor 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 callpath.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, andrequestTimeout, 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 byfs.access(filePath, ...)inwasm-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, nomaxConnections,headersTimeout, orrequestTimeouton 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
- CWE-22: Improper Limitation of a Pathname to a Restricted Directory
- CWE-400: Uncontrolled Resource Consumption
- OWASP Path Traversal Cheat Sheet
- OWASP Denial of Service Cheat Sheet
- Node.js
http.Serverdocumentation (timeouts and maxConnections) - Semgrep rules for path traversal
- fix: the http server processes all incoming requests... in server.js