Environment
- nitropack: 2.13.4 (
latest)
- h3: 1.15.11
- Node.js: 22.17.0
- preset:
node-server
- Reproduced on Windows 11. The same error also appears on a Linux production deployment.
Reproduction
https://github.com/sergiooak/nitro-host-header-error-handler
npm install
npm run build
node .output/server/index.mjs
# then, in another shell:
curl -s -H 'Host: a b' http://127.0.0.1:3000/x
Describe the bug
The default error handler builds the request URL from the raw Host header. When the header is not a valid URL authority, new URL() throws inside the handler, and the throw is not caught there.
Flow:
- A request arrives with a
Host header that new URL() cannot parse, for example a value containing a space.
- The route does not match, so Nitro runs the default error handler to render the 404.
-
defaultHandler calls getRequestURL(event, { xForwardedHost: true, xForwardedProto: true }).
-
getRequestURL runs new URL(path, \${protocol}://${host}`)with the rawHost. new URL()throwsTypeError [ERR_INVALID_URL]`.
- The throw escapes the error handler as a rejected promise.
trapUnhandledNodeErrors catches it and logs the full stack. The client gets a 404 with a reduced body. The process stays up.
Every request with such a Host logs one full stack trace.
Root cause:
- h3
getRequestURL builds the URL from the unvalidated Host header (getRequestHost returns event.node.req.headers.host).
- Nitro
defaultHandler (src/runtime/internal/error/prod.ts) calls getRequestURL with no try/catch, so a throw in the error handler is not contained.
Additional context
Which Host values trigger it: new URL('http://' + host) throws for authority values containing characters such as a space, [, ], <, %, or ::. It does not throw for " or /. Vulnerability scanners commonly send values like "></A></ADDRESS>"><script>alert(document.domain)</script><ADDRESS><A "/xss"', which throws because of the space.
Suggested fix: wrap the getRequestURL call in defaultHandler in try/catch and fall back to event.path, or validate the host in getRequestURL. The same class of bug was handled in @hono/node-server (CVE-2024-32652).
Production note: on a node-server deployment behind PM2, under sustained scanning we saw the service degrade until it stopped responding. A plain pm2 restart did not recover it; only pm2 delete plus a fresh start did. We have not isolated that exact mechanism in the minimal reproduction, which only shows the throw, the logged unhandled rejection, and the degraded response, with the process staying up. We are flagging it because an unauthenticated request reaches the error handler and every hit logs a full stack trace, which compounds at scanner volume.
Logs
TypeError: Invalid URL
at new URL (node:internal/url:818:25)
at getRequestURL (.output/server/chunks/_/nitro.mjs:763:10)
at defaultHandler (.output/server/chunks/_/nitro.mjs:4117:15)
at defaultNitroErrorHandler (.output/server/chunks/_/nitro.mjs:4107:17)
at errorHandler (.output/server/chunks/_/nitro.mjs:4171:13)
at Object.onError (.output/server/chunks/_/nitro.mjs:4407:14)
at Server.toNodeHandle (.output/server/chunks/_/nitro.mjs:1847:27)
at process.processTicksAndRejections (node:internal/process/task_queues:105:5) {
code: 'ERR_INVALID_URL',
input: '/x',
base: 'http://a b'
}
Environment
latest)node-serverReproduction
https://github.com/sergiooak/nitro-host-header-error-handler
Describe the bug
The default error handler builds the request URL from the raw
Hostheader. When the header is not a valid URL authority,new URL()throws inside the handler, and the throw is not caught there.Flow:
Hostheader thatnew URL()cannot parse, for example a value containing a space.defaultHandlercallsgetRequestURL(event, { xForwardedHost: true, xForwardedProto: true }).getRequestURLrunsnew URL(path, \${protocol}://${host}`)with the rawHost.new URL()throwsTypeError [ERR_INVALID_URL]`.trapUnhandledNodeErrorscatches it and logs the full stack. The client gets a 404 with a reduced body. The process stays up.Every request with such a
Hostlogs one full stack trace.Root cause:
getRequestURLbuilds the URL from the unvalidatedHostheader (getRequestHostreturnsevent.node.req.headers.host).defaultHandler(src/runtime/internal/error/prod.ts) callsgetRequestURLwith notry/catch, so a throw in the error handler is not contained.Additional context
Which
Hostvalues trigger it:new URL('http://' + host)throws for authority values containing characters such as a space,[,],<,%, or::. It does not throw for"or/. Vulnerability scanners commonly send values like"></A></ADDRESS>"><script>alert(document.domain)</script><ADDRESS><A "/xss"', which throws because of the space.Suggested fix: wrap the
getRequestURLcall indefaultHandlerintry/catchand fall back toevent.path, or validate the host ingetRequestURL. The same class of bug was handled in@hono/node-server(CVE-2024-32652).Production note: on a
node-serverdeployment behind PM2, under sustained scanning we saw the service degrade until it stopped responding. A plainpm2 restartdid not recover it; onlypm2 deleteplus a fresh start did. We have not isolated that exact mechanism in the minimal reproduction, which only shows the throw, the logged unhandled rejection, and the degraded response, with the process staying up. We are flagging it because an unauthenticated request reaches the error handler and every hit logs a full stack trace, which compounds at scanner volume.Logs