CVE-2024-32869: Insufficient Information (NVD-CWE-noinfo)
value: Hono is a Web application framework that provides support for any JavaScript runtime. Prior to version 4.2.7, when using serveStatic with deno, it is possible to traverse the directory where `main.ts` is located. This can result in retrieval of unexpected files. Version 4.2.7 contains a patch for the issue.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-04-23T21:15Z
Repository: https://github.com/honojs/hono
Language: TypeScript
Files changed: deno_dist/middleware/serve-static/index.ts, runtime_tests/deno/middleware.test.tsx, src/middleware/serve-static/index.test.ts, src/middleware/serve-static/index.ts
Commit date: 2024-04-23 17:14:16 +0900
Commit message: Merge pull request from GHSA-3mpf-rcc7-5347


Diff: diff --git a/deno_dist/middleware/serve-static/index.ts b/deno_dist/middleware/serve-static/index.ts
index fb261865..5dbf8de3 100644
--- a/deno_dist/middleware/serve-static/index.ts
+++ b/deno_dist/middleware/serve-static/index.ts
@@ -28,13 +28,12 @@ export const serveStatic = <E extends Env = Env>(
     // Do nothing if Response is already set
     if (c.finalized) {
       await next()
       return
     }
-    const url = new URL(c.req.url)
 
-    let filename = options.path ?? decodeURI(url.pathname)
+    let filename = options.path ?? decodeURI(c.req.path)
     filename = options.rewriteRequestPath ? options.rewriteRequestPath(filename) : filename
     const root = options.root
 
     let path = getFilePath({
       filename,
diff --git a/runtime_tests/deno/middleware.test.tsx b/runtime_tests/deno/middleware.test.tsx
index 6999e1ef..7f05805c 100644
--- a/runtime_tests/deno/middleware.test.tsx
+++ b/runtime_tests/deno/middleware.test.tsx
@@ -115,10 +115,17 @@ Deno.test('Serve Static middleware', async () => {
 
   res = await app.request('http://localhost/dot-static/plain.txt')
   assertEquals(res.status, 200)
   assertEquals(await res.text(), 'Deno!!')
   assertSpyCalls(onNotFound, 1)
+
+  res = await app.fetch({
+    method: 'GET',
+    url: 'http://localhost/static/%2e%2e/static/plain.txt',
+  } as Request)
+  assertEquals(res.status, 404)
+  assertEquals(await res.text(), '404 Not Found')
 })
 
 Deno.test('JWT Authentication middleware', async () => {
   const app = new Hono<{ Variables: { 'x-foo': string } }>()
   app.use('/*', async (c, next) => {
diff --git a/src/middleware/serve-static/index.test.ts b/src/middleware/serve-static/index.test.ts
new file mode 100644
index 00000000..cfc095bb
--- /dev/null
+++ b/src/middleware/serve-static/index.test.ts
@@ -0,0 +1,59 @@
+import { createMiddleware } from '../../helper'
+import { Hono } from '../../hono'
+import { serveStatic as baseServeStatic } from '.'
+
+describe('Serve Static Middleware', () => {
+  const app = new Hono()
+
+  const serveStatic = createMiddleware(async (c, next) => {
+    const mw = baseServeStatic({
+      getContent: (path) => {
+        if (path.endsWith('not-found.txt')) {
+          return undefined
+        }
+        return `Hello in ${path}`
+      },
+      pathResolve: (path) => {
+        return `./${path}`
+      },
+    })
+    return await mw(c, next)
+  })
+
+  app.get('/static/*', serveStatic)
+
+  it('Should return 200 response - /static/hello.html', async () => {
+    const res = await app.request('/static/hello.html')
+    expect(res.status).toBe(200)
+    expect(res.headers.get('Content-Type')).toMatch(/^text\/html/)
+    expect(await res.text()).toBe('Hello in ./static/hello.html')
+  })
+
+  it('Should return 200 response - /static/sub', async () => {
+    const res = await app.request('/static/sub')
+    expect(res.status).toBe(200)
+    expect(res.headers.get('Content-Type')).toMatch(/^text\/html/)
+    expect(await res.text()).toBe('Hello in ./static/sub/index.html')
+  })
+
+  it('Should decode URI strings - /static/%E7%82%8E.txt', async () => {
+    const res = await app.request('/static/%E7%82%8E.txt')
+    expect(res.status).toBe(200)
+    expect(await res.text()).toBe('Hello in ./static/炎.txt')
+  })
+
+  it('Should return 404 response - /static/not-found', async () => {
+    const res = await app.request('/static/not-found.txt')
+    expect(res.status).toBe(404)
+    expect(await res.text()).toBe('404 Not Found')
+  })
+
+  it('Should not allow a directory traversal - /static/%2e%2e/static/hello.html', async () => {
+    const res = await app.fetch({
+      method: 'GET',
+      url: 'http://localhost/static/%2e%2e/static/hello.html',
+    } as Request)
+    expect(res.status).toBe(404)
+    expect(await res.text()).toBe('404 Not Found')
+  })
+})
diff --git a/src/middleware/serve-static/index.ts b/src/middleware/serve-static/index.ts
index 3dafdc1b..5c2b87e9 100644
--- a/src/middleware/serve-static/index.ts
+++ b/src/middleware/serve-static/index.ts
@@ -28,13 +28,12 @@ export const serveStatic = <E extends Env = Env>(
     // Do nothing if Response is already set
     if (c.finalized) {
       await next()
       return
     }
-    const url = new URL(c.req.url)
 
-    let filename = options.path ?? decodeURI(url.pathname)
+    let filename = options.path ?? decodeURI(c.req.path)
     filename = options.rewriteRequestPath ? options.rewriteRequestPath(filename) : filename
     const root = options.root
 
     let path = getFilePath({
       filename,

Vulnerable code:     const url = new URL(c.req.url)
    let filename = options.path ?? decodeURI(url.pathname)
    const url = new URL(c.req.url)
    let filename = options.path ?? decodeURI(url.pathname)
Fixed code:     let filename = options.path ?? decodeURI(c.req.path)

  res = await app.fetch({
    method: 'GET',
    url: 'http://localhost/static/%2e%2e/static/plain.txt',
  } as Request)
  assertEquals(res.status, 404)
  assertEquals(await res.text(), '404 Not Found')
import { createMiddleware } from '../../helper'
import { Hono } from '../../hono'
import { serveStatic as baseServeStatic } from '.'

describe('Serve Static Middleware', () => {
  const app = new Hono()

  const serveStatic = createMiddleware(async (c, next) => {
    const mw = baseServeStatic({
      getContent: (path) => {
        if (path.endsWith('not-found.txt')) {
          return undefined
        }
        return `Hello in ${path}`
      },
      pathResolve: (path) => {
        return `./${path}`
      },
    })
    return await mw(c, next)
  })

  app.get('/static/*', serveStatic)

  it('Should return 200 response - /static/hello.html', async () => {
    const res = await app.request('/static/hello.html')
    expect(res.status).toBe(200)
    expect(res.headers.get('Content-Type')).toMatch(/^text\/html/)
    expect(await res.text()).toBe('Hello in ./static/hello.html')
  })

  it('Should return 200 response - /static/sub', async () => {
    const res = await app.request('/static/sub')
    expect(res.status).toBe(200)
    expect(res.headers.get('Content-Type')).toMatch(/^text\/html/)
    expect(await res.text()).toBe('Hello in ./static/sub/index.html')
  })

  it('Should decode URI strings - /static/%E7%82%8E.txt', async () => {
    const res = await app.request('/static/%E7%82%8E.txt')
    expect(res.status).toBe(200)
    expect(await res.text()).toBe('Hello in ./static/炎.txt')
  })

  it('Should return 404 response - /static/not-found', async () => {
    const res = await app.request('/static/not-found.txt')
    expect(res.status).toBe(404)
    expect(await res.text()).toBe('404 Not Found')
  })

  it('Should not allow a directory traversal - /static/%2e%2e/static/hello.html', async () => {
    const res = await app.fetch({
      method: 'GET',
      url: 'http://localhost/static/%2e%2e/static/hello.html',
    } as Request)
    expect(res.status).toBe(404)
    expect(await res.text()).toBe('404 Not Found')
  })
})
    let filename = options.path ?? decodeURI(c.req.path)