nitrojs / nitrojs/nitro

Default error handler throws ERR_INVALID_URL on an unparsable Host header

Open Beginner friendly
#4,555 5 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug v2
Dominant language
TypeScript
Stars
11.2k
Forks
899
Avg merge
2d 24m
Merged PRs (30d)
40

Description

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'
}

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Inspect src/runtime/internal/error/prod.ts and the defaultHandler call to getRequestURL; use the linked reproduction to confirm the failure with curl and the malformed Host header. Contain the URL-parsing failure using the suggested fallback, then rerun the reproduction and verify that the 404 does not emit an ERR_INVALID_URL stack trace.

Written by the indexing model from the issue text.

Assessment

Tech stack
node.js, typescript
Domain
api, backend
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.