Skip to content

Commit 455015b

Browse files
authored
Merge commit from fork
1 parent cc05c48 commit 455015b

File tree

3 files changed

+45
-2
lines changed

3 files changed

+45
-2
lines changed

src/serve-static.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,25 @@ const getStats = (path: string) => {
6868
return stats
6969
}
7070

71+
type Decoder = (str: string) => string
72+
73+
const tryDecode = (str: string, decoder: Decoder): string => {
74+
try {
75+
return decoder(str)
76+
} catch {
77+
// Decode only valid %xx sequences in chunks; keep undecodable parts as-is
78+
return str.replace(/(?:%[0-9A-Fa-f]{2})+/g, (match) => {
79+
try {
80+
return decoder(match)
81+
} catch {
82+
return match
83+
}
84+
})
85+
}
86+
}
87+
88+
const tryDecodeURI = (str: string) => tryDecode(str, decodeURI)
89+
7190
// eslint-disable-next-line @typescript-eslint/no-explicit-any
7291
export const serveStatic = <E extends Env = any>(
7392
options: ServeStaticOptions<E> = { root: '' }
@@ -91,7 +110,7 @@ export const serveStatic = <E extends Env = any>(
91110
filename = optionPath
92111
} else {
93112
try {
94-
filename = decodeURIComponent(c.req.path)
113+
filename = tryDecodeURI(c.req.path)
95114
if (/(?:^|[\/\\])\.\.(?:$|[\/\\])/.test(filename)) {
96115
throw new Error()
97116
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
secret

test/serve-static.test.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ describe('Serve Static Middleware', () => {
330330
})
331331
})
332332

333-
describe('Security tests', () => {
333+
describe('Path traversal security tests', () => {
334334
const app = new Hono()
335335
const server = createAdaptorServer(app)
336336
app.use('/static/*', serveStatic({ root: './test/assets' }))
@@ -361,6 +361,29 @@ describe('Serve Static Middleware', () => {
361361
})
362362
})
363363

364+
describe('Path mismatch security tests', () => {
365+
const app = new Hono()
366+
const server = createAdaptorServer(app)
367+
368+
app.use('/static/admin/*', async (c, next) => {
369+
c.header('X-Authorized', 'true')
370+
await next()
371+
})
372+
373+
app.use('/static/*', serveStatic({ root: './test/assets' }))
374+
375+
it('Should not allow bypass via path mismatch between middleware and serveStatic', async () => {
376+
const res = await request(server).get('/static/admin/secret.txt')
377+
expect(res.headers['x-authorized']).toBe('true')
378+
expect(res.text).toBe('secret')
379+
380+
const res2 = await request(server).get('/static/admin%2Fsecret.txt')
381+
expect(res2.status).toBe(404)
382+
expect(res2.headers['x-authorized']).toBeUndefined()
383+
expect(res2.text).not.toBe('secret')
384+
})
385+
})
386+
364387
describe('Stream error handling', () => {
365388
const testFile = path.join(__dirname, 'assets', 'static', 'plain.txt')
366389
console.log(testFile)

0 commit comments

Comments
 (0)