From 89d917a3e77a1a4b2b56572c2096f9809ab186fc Mon Sep 17 00:00:00 2001 From: Taku Amano Date: Sat, 19 Jul 2025 11:23:20 +0900 Subject: [PATCH 1/3] fix: enable to serve filename with double dots --- src/serve-static.ts | 2 +- test/serve-static.test.ts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/serve-static.ts b/src/serve-static.ts index d841f75..9aaef4e 100644 --- a/src/serve-static.ts +++ b/src/serve-static.ts @@ -72,7 +72,7 @@ export const serveStatic = ( // Prevent encoded path traversal attacks if (!optionPath) { const decodedPath = decodeURIComponent(rawPath) - if (decodedPath.includes('..')) { + if (/(?:^|[\/\\])\.\.(?:$|[\/\\])/.test(decodedPath)) { await options.onNotFound?.(rawPath, c) return next() } diff --git a/test/serve-static.test.ts b/test/serve-static.test.ts index 1e9c5a3..cc34f03 100644 --- a/test/serve-static.test.ts +++ b/test/serve-static.test.ts @@ -318,5 +318,10 @@ describe('Serve Static Middleware', () => { const res = await request(server).get('/static/%2e%2e%2fsecret.txt') expect(res.status).toBe(404) }) + + it('Should accept filename with double dots', async () => { + const res = await request(server).get('/static/foo..bar.txt') + expect(res.status).toBe(200) + }) }) }) From afaa51c1ec913483172d66265c66bfa4636c48a5 Mon Sep 17 00:00:00 2001 From: Taku Amano Date: Sat, 19 Jul 2025 11:25:21 +0900 Subject: [PATCH 2/3] refactor: simplify serve-static() function --- src/serve-static.ts | 48 ++++++++++++++------------------------- test/serve-static.test.ts | 4 ++-- 2 files changed, 19 insertions(+), 33 deletions(-) diff --git a/src/serve-static.ts b/src/serve-static.ts index 9aaef4e..9cf9dc8 100644 --- a/src/serve-static.ts +++ b/src/serve-static.ts @@ -2,7 +2,7 @@ import type { Context, Env, MiddlewareHandler } from 'hono' import { getMimeType } from 'hono/utils/mime' import type { ReadStream, Stats } from 'node:fs' import { createReadStream, lstatSync } from 'node:fs' -import { join, resolve } from 'node:path' +import { join } from 'node:path' export type ServeStaticOptions = { /** @@ -56,7 +56,7 @@ const getStats = (path: string) => { export const serveStatic = ( options: ServeStaticOptions = { root: '' } ): MiddlewareHandler => { - const root = resolve(options.root || '.') + const root = options.root || '' const optionPath = options.path return async (c, next) => { @@ -67,44 +67,30 @@ export const serveStatic = ( let filename: string - try { - const rawPath = optionPath ?? c.req.path - // Prevent encoded path traversal attacks - if (!optionPath) { - const decodedPath = decodeURIComponent(rawPath) - if (/(?:^|[\/\\])\.\.(?:$|[\/\\])/.test(decodedPath)) { - await options.onNotFound?.(rawPath, c) - return next() + if (optionPath) { + filename = optionPath + } else { + try { + filename = decodeURIComponent(c.req.path) + if (/(?:^|[\/\\])\.\.(?:$|[\/\\])/.test(filename)) { + throw new Error() } + } catch { + await options.onNotFound?.(c.req.path, c) + return next() } - filename = optionPath ?? decodeURIComponent(c.req.path) - } catch { - await options.onNotFound?.(c.req.path, c) - return next() } - const requestPath = options.rewriteRequestPath - ? options.rewriteRequestPath(filename, c) - : filename - - let path = optionPath - ? options.root - ? resolve(join(root, optionPath)) - : optionPath - : resolve(join(root, requestPath)) + let path = join( + root, + !optionPath && options.rewriteRequestPath ? options.rewriteRequestPath(filename, c) : filename + ) let stats = getStats(path) if (stats && stats.isDirectory()) { const indexFile = options.index ?? 'index.html' - path = resolve(join(path, indexFile)) - - // Security check: prevent path traversal attacks - if (!optionPath && !path.startsWith(root)) { - await options.onNotFound?.(path, c) - return next() - } - + path = join(path, indexFile) stats = getStats(path) } diff --git a/test/serve-static.test.ts b/test/serve-static.test.ts index cc34f03..a0fa603 100644 --- a/test/serve-static.test.ts +++ b/test/serve-static.test.ts @@ -69,7 +69,7 @@ describe('Serve Static Middleware', () => { expect(res.text).toBe('

Hello Hono

') expect(res.headers['content-type']).toBe('text/html; charset=utf-8') expect(res.headers['x-custom']).toMatch( - /Found the file at .*[\/\\]test[\/\\]assets[\/\\]static[\/\\]index\.html$/ + /Found the file at test[\/\\]assets[\/\\]static[\/\\]index\.html$/ ) }) @@ -170,7 +170,7 @@ describe('Serve Static Middleware', () => { const res = await request(server).get('/on-not-found/foo.txt') expect(res.status).toBe(404) expect(notFoundMessage).toMatch( - /.*[\/\\]not-found[\/\\]on-not-found[\/\\]foo\.txt is not found, request to \/on-not-found\/foo\.txt$/ + /not-found[\/\\]on-not-found[\/\\]foo\.txt is not found, request to \/on-not-found\/foo\.txt$/ ) }) From 1abcca32d85896ede8864f4070768276355a9b0f Mon Sep 17 00:00:00 2001 From: Taku Amano Date: Sat, 19 Jul 2025 11:42:34 +0900 Subject: [PATCH 3/3] test: add file foo..bar.txt --- test/assets/static/foo..bar.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/assets/static/foo..bar.txt diff --git a/test/assets/static/foo..bar.txt b/test/assets/static/foo..bar.txt new file mode 100644 index 0000000..e69de29