CVE-2024-32652: Insufficient Information (NVD-CWE-noinfo)
value: The adapter @hono/node-server allows you to run your Hono application on Node.js. Prior to 1.10.1, the application hangs when receiving a Host header with a value that `@hono/node-server` can't handle well. Invalid values are those that cannot be parsed by the `URL` as a hostname such as an empty string, slashes `/`, and other strings. The version 1.10.1 includes the fix for this issue.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-04-19T19:15Z
Repository: https://github.com/honojs/node-server
Language: TypeScript
Files changed: src/listener.ts, test/listener.test.ts
Commit date: 2024-04-19 14:48:36 +0900
Commit message: fix: catch ERR_INVALID_URL error in listener (#162)


Diff: diff --git a/src/listener.ts b/src/listener.ts
index 82655d2..67c3c26 100644
--- a/src/listener.ts
+++ b/src/listener.ts
@@ -157,22 +157,22 @@ export const getRequestListener = (
     incoming: IncomingMessage | Http2ServerRequest,
     outgoing: ServerResponse | Http2ServerResponse
   ) => {
     let res
 
-    // `fetchCallback()` requests a Request object, but global.Request is expensive to generate,
-    // so generate a pseudo Request object with only the minimum required information.
-    const req = newRequest(incoming)
-
-    // Detect if request was aborted.
-    outgoing.on('close', () => {
-      if (incoming.destroyed) {
-        req[getAbortController]().abort()
-      }
-    })
-
     try {
+      // `fetchCallback()` requests a Request object, but global.Request is expensive to generate,
+      // so generate a pseudo Request object with only the minimum required information.
+      const req = newRequest(incoming)
+
+      // Detect if request was aborted.
+      outgoing.on('close', () => {
+        if (incoming.destroyed) {
+          req[getAbortController]().abort()
+        }
+      })
+
       res = fetchCallback(req, { incoming, outgoing } as HttpBindings) as
         | Response
         | Promise<Response>
       if (cacheKey in res) {
         // synchronous, cacheable response
diff --git a/test/listener.test.ts b/test/listener.test.ts
index 58973a2..65ac1c0 100644
--- a/test/listener.test.ts
+++ b/test/listener.test.ts
@@ -2,10 +2,32 @@ import { createServer } from 'node:http'
 import request from 'supertest'
 import { getRequestListener } from '../src/listener'
 import { GlobalRequest, Request as LightweightRequest } from '../src/request'
 import { GlobalResponse, Response as LightweightResponse } from '../src/response'
 
+describe('Invalid request', () => {
+  const requestListener = getRequestListener(jest.fn())
+  const server = createServer(async (req, res) => {
+    await requestListener(req, res)
+
+    if (!res.writableEnded) {
+      res.writeHead(500, { 'Content-Type': 'text/plain' })
+      res.end('error handler did not return a response')
+    }
+  })
+
+  it('Should return server error for a request w/o host header', async () => {
+    const res = await request(server).get('/').set('Host', '').send()
+    expect(res.status).toBe(500)
+  })
+
+  it('Should return server error for a request invalid host header', async () => {
+    const res = await request(server).get('/').set('Host', 'a b').send()
+    expect(res.status).toBe(500)
+  })
+})
+
 describe('Error handling - sync fetchCallback', () => {
   const fetchCallback = jest.fn(() => {
     throw new Error('thrown error')
   })
   const errorHandler = jest.fn()

Vulnerable code:     // `fetchCallback()` requests a Request object, but global.Request is expensive to generate,
    // so generate a pseudo Request object with only the minimum required information.
    const req = newRequest(incoming)

    // Detect if request was aborted.
    outgoing.on('close', () => {
      if (incoming.destroyed) {
        req[getAbortController]().abort()
      }
    })

Fixed code:       // `fetchCallback()` requests a Request object, but global.Request is expensive to generate,
      // so generate a pseudo Request object with only the minimum required information.
      const req = newRequest(incoming)

      // Detect if request was aborted.
      outgoing.on('close', () => {
        if (incoming.destroyed) {
          req[getAbortController]().abort()
        }
      })

describe('Invalid request', () => {
  const requestListener = getRequestListener(jest.fn())
  const server = createServer(async (req, res) => {
    await requestListener(req, res)

    if (!res.writableEnded) {
      res.writeHead(500, { 'Content-Type': 'text/plain' })
      res.end('error handler did not return a response')
    }
  })

  it('Should return server error for a request w/o host header', async () => {
    const res = await request(server).get('/').set('Host', '').send()
    expect(res.status).toBe(500)
  })

  it('Should return server error for a request invalid host header', async () => {
    const res = await request(server).get('/').set('Host', 'a b').send()
    expect(res.status).toBe(500)
  })
})