Skip to content

Default error handler throws ERR_INVALID_URL on an unparsable Host header #4555

Description

@sergiooak

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:

  1. A request arrives with a Host header that new URL() cannot parse, for example a value containing a space.
  2. The route does not match, so Nitro runs the default error handler to render the 404.
  3. defaultHandler calls getRequestURL(event, { xForwardedHost: true, xForwardedProto: true }).
  4. getRequestURL runs new URL(path, \${protocol}://${host}`)with the rawHost. new URL()throwsTypeError [ERR_INVALID_URL]`.
  5. 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'
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions