-
-
Notifications
You must be signed in to change notification settings - Fork 969
feat(serve-static): use join to correct path resolution
#4291
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 12 commits
8c742b5
3ebf6fe
4fdae05
143f7a1
60e3c84
3473d75
937d169
62af5de
28d8268
03fc8f7
4ad3d85
b3738f7
f3ee780
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| import { join } from 'node:path' | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can use @std/path, but it's better to use |
||
| import type { ServeStaticOptions } from '../../middleware/serve-static' | ||
| import { serveStatic as baseServeStatic } from '../../middleware/serve-static' | ||
| import type { Env, MiddlewareHandler } from '../../types' | ||
|
|
@@ -23,9 +24,6 @@ export const serveStatic = <E extends Env = Env>( | |
| return null | ||
| } | ||
| } | ||
| const pathResolve = (path: string) => { | ||
| return path.startsWith('/') ? path : `./${path}` | ||
| } | ||
| const isDir = (path: string) => { | ||
| let isDir | ||
| try { | ||
|
|
@@ -34,11 +32,10 @@ export const serveStatic = <E extends Env = Env>( | |
| } catch {} | ||
| return isDir | ||
| } | ||
|
|
||
| return baseServeStatic({ | ||
| ...options, | ||
| getContent, | ||
| pathResolve, | ||
| join, | ||
| isDir, | ||
| })(c, next) | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,11 +12,10 @@ describe('Serve Static Middleware', () => { | |
|
|
||
| const serveStatic = baseServeStatic({ | ||
| getContent, | ||
| pathResolve: (path) => { | ||
| return `./${path}` | ||
| }, | ||
| isDir: (path) => { | ||
| return path === 'static/hello.world' | ||
| if (path === 'static/sub' || path === 'static/hello.world') { | ||
| return true | ||
| } | ||
| }, | ||
| onFound: (path, c) => { | ||
| if (path.endsWith('hello.html')) { | ||
|
|
@@ -36,36 +35,29 @@ describe('Serve Static Middleware', () => { | |
| expect(res.status).toBe(200) | ||
| expect(res.headers.get('Content-Encoding')).toBeNull() | ||
| expect(res.headers.get('Content-Type')).toMatch(/^text\/html/) | ||
| expect(await res.text()).toBe('Hello in ./static/hello.html') | ||
| expect(res.headers.get('X-Custom')).toBe('Found the file at ./static/hello.html') | ||
| expect(await res.text()).toBe('Hello in static/hello.html') | ||
| expect(res.headers.get('X-Custom')).toBe('Found the file at 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 return 200 response - /static/helloworld', async () => { | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test is the same as |
||
| const res = await app.request('/static/helloworld') | ||
| expect(res.status).toBe(200) | ||
| expect(res.headers.get('Content-Type')).toMatch(/^text\/html/) | ||
| expect(await res.text()).toBe('Hello in ./static/helloworld/index.html') | ||
| expect(await res.text()).toBe('Hello in static/sub/index.html') | ||
| }) | ||
|
|
||
| it('Should return 200 response - /static/hello.world', async () => { | ||
| const res = await app.request('/static/hello.world') | ||
| expect(res.status).toBe(200) | ||
| expect(res.headers.get('Content-Type')).toMatch(/^text\/html/) | ||
| expect(await res.text()).toBe('Hello in ./static/hello.world/index.html') | ||
| expect(await res.text()).toBe('Hello in static/hello.world/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(res.headers.get('Content-Type')).toMatch(/^text\/plain/) | ||
| expect(await res.text()).toBe('Hello in ./static/炎.txt') | ||
| expect(await res.text()).toBe('Hello in static/炎.txt') | ||
| }) | ||
|
|
||
| it('Should return 404 response - /static/not-found.txt', async () => { | ||
|
|
@@ -228,15 +220,10 @@ describe('Serve Static Middleware', () => { | |
| }) | ||
|
|
||
| describe('Changing root path', () => { | ||
| const pathResolve = (path: string) => { | ||
| return path.startsWith('/') ? path : `./${path}` | ||
| } | ||
|
|
||
| it('Should return the content with absolute root path', async () => { | ||
| const app = new Hono() | ||
| const serveStatic = baseServeStatic({ | ||
| getContent, | ||
| pathResolve, | ||
| root: '/home/hono/child', | ||
| }) | ||
| app.get('/static/*', serveStatic) | ||
|
|
@@ -249,7 +236,6 @@ describe('Serve Static Middleware', () => { | |
| const app = new Hono() | ||
| const serveStatic = baseServeStatic({ | ||
| getContent, | ||
| pathResolve, | ||
| root: '/home/hono/../parent', | ||
| }) | ||
| app.get('/static/*', serveStatic) | ||
|
|
@@ -262,26 +248,24 @@ describe('Serve Static Middleware', () => { | |
| const app = new Hono() | ||
| const serveStatic = baseServeStatic({ | ||
| getContent, | ||
| pathResolve, | ||
| root: '../home/hono', | ||
| }) | ||
| app.get('/static/*', serveStatic) | ||
|
|
||
| const res = await app.request('/static/html/hello.html') | ||
| expect(await res.text()).toBe('Hello in ./../home/hono/static/html/hello.html') | ||
| expect(await res.text()).toBe('Hello in ../home/hono/static/html/hello.html') | ||
| }) | ||
|
|
||
| it('Should not allow directory traversal with . as relative path', async () => { | ||
| const app = new Hono() | ||
| const serveStatic = baseServeStatic({ | ||
| getContent, | ||
| pathResolve, | ||
| root: '.', | ||
| }) | ||
| app.get('*', serveStatic) | ||
|
|
||
| const res = await app.request('///etc/passwd') | ||
| expect(res.status).toBe(404) | ||
| expect(await res.text()).toBe('Hello in etc/passwd') | ||
| }) | ||
| }) | ||
| }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| import { join as posixJoin } from 'node:path/posix' | ||
| import { defaultJoin } from './path' | ||
|
|
||
| describe('defaultJoin', () => { | ||
| describe('Comparison with node:path/posix.join', () => { | ||
| it('Should behave like path.posix.join for all path operations', () => { | ||
| const testCases = [ | ||
| // Basic path joining | ||
| ['/home/yusuke/work/app/public', 'static/main.html'], | ||
| ['public', 'sub/', 'file.html'], | ||
| ['', 'file.html'], | ||
| ['public', ''], | ||
| ['public/', 'file.html'], | ||
| ['public', 'static', 'main.html'], | ||
| ['assets', 'images', 'logo.png'], | ||
|
|
||
| // Parent directory references | ||
| ['public', '../parent/file.html'], | ||
| ['public', '../../grandparent/file.html'], | ||
| ['/abs/path', '../relative.html'], | ||
| ['a/b', '../c'], | ||
| ['a/b/c', '../../d'], | ||
|
|
||
| // Current directory references | ||
| ['./public', 'static/main.html'], | ||
| ['public', './file.html'], | ||
| ['./public', '/absolute/path.html'], | ||
| ['.', 'file.html'], | ||
| ['public/.', 'file.html'], | ||
|
|
||
| // Edge cases | ||
| [], | ||
| ['.'], | ||
| [''], | ||
| ['/'], | ||
| ['a', 'b', 'c'], | ||
|
|
||
| // Backslash handling (security) | ||
| ['static', 'test\\file.txt'], | ||
| ['public', 'path\\with\\backslash'], | ||
| ] | ||
|
|
||
| testCases.forEach(([...args]) => { | ||
| const expected = posixJoin(...args) | ||
| const actual = defaultJoin(...args) | ||
| expect(actual).toBe(expected) | ||
| }) | ||
| }) | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the previous implementation, if the file was not found, it checked whether
index.htmlexisted. This PR, that process has been removed.Since serve static for Cloudflare Workers is deprecated and not planned to be used, this change is acceptable.