From 3d5a0e24e6a16b159663cf9f5993279c82e39bec Mon Sep 17 00:00:00 2001 From: BlankParticle Date: Thu, 16 Jul 2026 12:45:32 +0530 Subject: [PATCH] fix: use a custom helper for path traversal tests --- src/request.ts | 2 +- test/helpers/request.ts | 53 +++++++++++++++++++++++++++++++++++++++ test/serve-static.test.ts | 45 ++++++++++++++++++++++++++++++--- 3 files changed, 95 insertions(+), 5 deletions(-) create mode 100644 test/helpers/request.ts diff --git a/src/request.ts b/src/request.ts index e963fd9..1519fe2 100644 --- a/src/request.ts +++ b/src/request.ts @@ -43,7 +43,7 @@ export class Request extends GlobalRequest { } } -const newHeadersFromIncoming = (incoming: IncomingMessage | Http2ServerRequest) => { +export const newHeadersFromIncoming = (incoming: IncomingMessage | Http2ServerRequest) => { const headerRecord: [string, string][] = [] const rawHeaders = incoming.rawHeaders for (let i = 0, len = rawHeaders.length; i < len; i += 2) { diff --git a/test/helpers/request.ts b/test/helpers/request.ts new file mode 100644 index 0000000..10ec0a8 --- /dev/null +++ b/test/helpers/request.ts @@ -0,0 +1,53 @@ +import { once } from 'node:events' +import { request as requestHTTP } from 'node:http' +import type { IncomingMessage, RequestOptions } from 'node:http' +import type { AddressInfo } from 'node:net' +import { Readable } from 'node:stream' +import { newHeadersFromIncoming } from '../../src/request' +import { GlobalResponse } from '../../src/response' +import type { ServerType } from '../../src/types' + +export type ServerRequestInit = Omit< + RequestOptions, + 'agent' | 'host' | 'hostname' | 'path' | 'port' | 'protocol' +> & { + body?: string | Uint8Array + path: string +} + +export const requestServer = async ( + server: ServerType, + init: ServerRequestInit +): Promise => { + const address = server.address() as AddressInfo | null + if (!address) { + throw new Error('Server is not listening on a TCP address') + } + + const { body, path, ...options } = init + const request = requestHTTP({ + ...options, + agent: false, + hostname: address.address, + path, + port: address.port, + }) + request.end(body) + + const [incoming] = (await once(request, 'response')) as [IncomingMessage] + const status = incoming.statusCode + if (!status) { + throw new Error('Server response did not include a status code') + } + + const responseBody = + options.method?.toUpperCase() === 'HEAD' || [101, 204, 205, 304].includes(status) + ? null + : (Readable.toWeb(incoming) as ReadableStream) + + return new GlobalResponse(responseBody, { + headers: newHeadersFromIncoming(incoming), + status, + statusText: incoming.statusMessage, + }) +} diff --git a/test/serve-static.test.ts b/test/serve-static.test.ts index 681347a..f0e2417 100644 --- a/test/serve-static.test.ts +++ b/test/serve-static.test.ts @@ -4,6 +4,7 @@ import { chmodSync, rmSync, statSync, symlinkSync } from 'node:fs' import path from 'node:path' import { serveStatic } from './../src/serve-static' import { createAdaptorServer } from './../src/server' +import { requestServer } from './helpers/request' describe('Serve Static Middleware', () => { const app = new Hono<{ @@ -67,6 +68,21 @@ describe('Serve Static Middleware', () => { const server = createAdaptorServer(app) + beforeAll( + () => + new Promise((resolve, reject) => { + server.once('error', reject) + server.listen(0, '127.0.0.1', resolve) + }) + ) + + afterAll( + () => + new Promise((resolve, reject) => { + server.close((error) => (error ? reject(error) : resolve())) + }) + ) + it('Should return index.html', async () => { const res = await request(server).get('/static/') expect(res.status).toBe(200) @@ -271,7 +287,7 @@ describe('Serve Static Middleware', () => { }) it('Should handle double dots in URL', async () => { - const res = await request(server).get('/static/../secret.txt') + const res = await requestServer(server, { method: 'GET', path: '/static/../secret.txt' }) expect(res.status).toBe(404) }) @@ -428,18 +444,39 @@ describe('Serve Static Middleware', () => { const server = createAdaptorServer(app) app.use('/static/*', serveStatic({ root: './test/assets' })) + beforeAll( + () => + new Promise((resolve, reject) => { + server.once('error', reject) + server.listen(0, '127.0.0.1', resolve) + }) + ) + + afterAll( + () => + new Promise((resolve, reject) => { + server.close((error) => (error ? reject(error) : resolve())) + }) + ) + it('Should prevent path traversal attacks with double dots', async () => { - const res = await request(server).get('/static/../secret.txt') + const res = await requestServer(server, { method: 'GET', path: '/static/../secret.txt' }) expect(res.status).toBe(404) }) it('Should prevent path traversal attacks with multiple levels', async () => { - const res = await request(server).get('/static/../../package.json') + const res = await requestServer(server, { + method: 'GET', + path: '/static/../../package.json', + }) expect(res.status).toBe(404) }) it('Should prevent path traversal attacks with mixed separators', async () => { - const res = await request(server).get('/static/..\\..\\package.json') + const res = await requestServer(server, { + method: 'GET', + path: '/static/..\\..\\package.json', + }) expect(res.status).toBe(404) })