Summary
An unauthenticated request with a malformed path, for example GET //, crashes the
entire Node process. createRequest() builds new URL(req.url, base) with no guard,
and new URL("//", base) throws ERR_INVALID_URL. That throw is not caught anywhere
in the request dispatch path, so it surfaces as an uncaught exception and kills the
process. In a container this is a crash + restart on every such request, and scanners
and bots send // routinely, so it recurs on its own. This is effectively an
unauthenticated remote denial of service and affects any tina4-nodejs app.
Version observed: tina4-nodejs 3.13.70 on Node 22 (also reproduces on Node 24).
Reproduce
Start any tina4-nodejs server, then send a raw request with a // path. curl
normalizes the path, so use a raw socket:
printf 'GET // HTTP/1.1\r\nHost: localhost\r\n\r\n' | nc localhost 7148
The process exits with:
TypeError: Invalid URL
at new URL (node:internal/url)
at createRequest (packages/core/src/request.ts:49)
at Server.dispatch (packages/core/src/server.ts:1051)
at Server.emit (node:events)
at parserOnIncoming (node:_http_server)
code: 'ERR_INVALID_URL',
input: '//',
base: 'https://your-host'
Confirmed in isolation:
new URL("//", "https://example.com") // throws ERR_INVALID_URL
Root cause
packages/core/src/request.ts:49
const url = new URL(req.url ?? "/", `${proto}://${host}`);
req.url is client controlled. A value of // (and other malformed paths) makes
new URL throw. This call runs at the very top of dispatch()
(server.ts:1051, const req = createRequest(rawReq);) with no surrounding
try/catch, so the throw escapes the request handler and becomes uncaught.
Proposed fix
Two layers of defense.
- Guard the parse in
createRequest so a malformed path falls back to /:
let url: URL;
try {
url = new URL(req.url ?? "/", `${proto}://${host}`);
} catch {
url = new URL("/", `${proto}://${host}`);
}
- Wrap the
dispatch() body in try/catch so any per-request throw returns a 400
instead of taking down the process:
async function dispatch(rawReq: IncomingMessage, rawRes: ServerResponse): Promise<void> {
try {
// ... existing body ...
} catch (e) {
try { rawRes.statusCode = 400; rawRes.end("Bad Request"); } catch { /* socket gone */ }
}
}
Parity
The comment on request.ts:49 claims parity with PHP/Python/Ruby. Those most likely
tolerate a malformed path already, so this looks like a Node-only regression. Worth a
cross-framework check so all four behave the same (return 400, stay up).
Suggested regression test (no mocks)
Boot a real server, open a raw TCP socket, send GET // HTTP/1.1, assert the response
status is 400 (not a dropped connection), then send a normal GET /health on a new
connection to prove the process is still alive.
Workaround in the meantime
App-level process.on("uncaughtException", ...) keeps the process alive, but that is a
band-aid: the offending request gets no response and the process state after an uncaught
exception is undefined. The framework fix above is the correct place to handle it.
Summary
An unauthenticated request with a malformed path, for example
GET //, crashes theentire Node process.
createRequest()buildsnew URL(req.url, base)with no guard,and
new URL("//", base)throwsERR_INVALID_URL. That throw is not caught anywherein the request dispatch path, so it surfaces as an uncaught exception and kills the
process. In a container this is a crash + restart on every such request, and scanners
and bots send
//routinely, so it recurs on its own. This is effectively anunauthenticated remote denial of service and affects any tina4-nodejs app.
Version observed: tina4-nodejs 3.13.70 on Node 22 (also reproduces on Node 24).
Reproduce
Start any tina4-nodejs server, then send a raw request with a
//path. curlnormalizes the path, so use a raw socket:
The process exits with:
Confirmed in isolation:
Root cause
packages/core/src/request.ts:49req.urlis client controlled. A value of//(and other malformed paths) makesnew URLthrow. This call runs at the very top ofdispatch()(
server.ts:1051,const req = createRequest(rawReq);) with no surroundingtry/catch, so the throw escapes the request handler and becomes uncaught.
Proposed fix
Two layers of defense.
createRequestso a malformed path falls back to/:dispatch()body in try/catch so any per-request throw returns a 400instead of taking down the process:
Parity
The comment on
request.ts:49claims parity with PHP/Python/Ruby. Those most likelytolerate a malformed path already, so this looks like a Node-only regression. Worth a
cross-framework check so all four behave the same (return 400, stay up).
Suggested regression test (no mocks)
Boot a real server, open a raw TCP socket, send
GET // HTTP/1.1, assert the responsestatus is
400(not a dropped connection), then send a normalGET /healthon a newconnection to prove the process is still alive.
Workaround in the meantime
App-level
process.on("uncaughtException", ...)keeps the process alive, but that is aband-aid: the offending request gets no response and the process state after an uncaught
exception is undefined. The framework fix above is the correct place to handle it.