Skip to content

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

Open
wants to merge 12 commits into
base: main
Choose a base branch
from

Conversation

yusukebe
Copy link
Member

@yusukebe yusukebe commented Jul 20, 2025

This PR introduces a new mechanism for the serve static middleware. This can fix unintended path resolution.

  • Duplicated pathResolve option
  • Added join option
  • Simplified src/middleware/serve-static/index.ts
  • Implemented defaultJoin. It is used if the join option is not specified
  • Updated serve static for deno/bun
  • Fixed some tests to support new functions

It is inspired by honojs/node-server#261

Problems

In previous implementations, paths starting with C:\Users\yusuke\ on Windows were converted to /Users/yusuke. This caused unintended behavior because the drive name was lost.

If we use a function such as join exported by path:node, we can solve this problem and simplify the code. The pathResolve option is also unnecessary.

Breaking changes?

I changed the test code, but only path passed to onFound and onNotFound has been slightly changed.

Other behaviors remain unchanged. Additionally, unexpected path resolution issues should be resolved immediately. Release a minor version without changing the major version.

The author should do the following, if applicable

  • Add tests
  • Run tests
  • bun run format:fix && bun run lint:fix to format the code
  • Add TSDoc/JSDoc to document the code

@yusukebe yusukebe marked this pull request as draft July 20, 2025 09:24

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

Copy link

codecov bot commented Jul 23, 2025

Codecov Report

❌ Patch coverage is 85.93750% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.52%. Comparing base (89f4c96) to head (b3738f7).
⚠️ Report is 25 commits behind head on main.

Files with missing lines Patch % Lines
src/middleware/serve-static/path.ts 78.94% 8 Missing ⚠️
src/middleware/serve-static/index.ts 95.45% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4291      +/-   ##
==========================================
- Coverage   91.61%   91.52%   -0.10%     
==========================================
  Files         170      171       +1     
  Lines       10875    10792      -83     
  Branches     3099     3113      +14     
==========================================
- Hits         9963     9877      -86     
- Misses        911      914       +3     
  Partials        1        1              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

This comment has been minimized.

This comment has been minimized.

@@ -79,7 +79,8 @@ describe('ServeStatic Middleware', () => {
expect(res.headers.get('Content-Type')).toBe('text/plain; charset=utf-8')
})

it('Should return index.html', async () => {
// Serve static on Cloudflare Workers cannot determine whether the target path is a directory or not
it.skip('Should return index.html', async () => {
Copy link
Member Author

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.html existed. 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.

@yusukebe yusukebe marked this pull request as ready for review July 23, 2025 11:24
@yusukebe
Copy link
Member Author

Hey @usualoma !

What do you think of this? If it makes sense for you, please review it!

@usualoma
Copy link
Member

Hi @yusukebe
Thanks for all your hard work! Just give me a few days to review everything.

@yusukebe
Copy link
Member Author

@usualoma Okay!

@usualoma
Copy link
Member

Hi @yusukebe

Reasons for not using ‘node:path’ in deno

I think the reason is probably because we haven't used modules in the "node:" namespace in this file before, but is that correct? (We have already used them in bun.)

If that is the case, as mentioned below, join processing is difficult, so I think it would be safer to depend on nodo:path even in Deno.

I think it's better to always use / for separators.

Currently, if an attacker inserts \ into the request string, the entire string will be joined with \ , which may cause the path restriction to fail. In a Windows environment, / should be joined as expected, so I think we should always use / .

const app = new Hono()

app.get('/', (c) => {
  return c.text('Hello Hono!')
})

app.use('/static/*', serveStatic({ root: './' }))

export default app
% curl http://0.0.0.0:8000/static/test%5Ctest.txt

When this app is run in a Linux environment, it will search for the following file.

static\test\test.txt

In a Windows environment, this file is located in the static directory, but in a Linux environment, it is located in the current directory as static\test\test.txt.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

Copy link

github-actions bot commented Aug 2, 2025

Bundle size check

main (23c6d5a) #4291 (b6b9ab1) +/-
Bundle Size (B) 18,320B 18,320B 0B
Bundle Size (KB) 17.89K 17.89K 0K

Compiler Diagnostics (tsc)

main (23c6d5a) #4291 (b6b9ab1) +/-
Files 262 262 0
Lines 116,395 116,395 0
Identifiers 114,321 114,321 0
Symbols 259,914 259,914 0
Types 162,579 162,579 0
Instantiations 3,037,134 3,037,134 0
Memory used 273,306K 271,128K -2,178K
I/O read 0.02s 0.04s 0.02s
I/O write 0s 0s 0s
Parse time 0.76s 0.69s -0.07s
Bind time 0.33s 0.28s -0.05s
Check time 3.98s 3.65s -0.33s
Emit time 0s 0s 0s
Total time 5.07s 4.62s -0.45s

Compiler Diagnostics (typescript-go)

main (23c6d5a) #4291 (b6b9ab1) +/-
Files 232 232 0
Lines 106,294 106,294 0
Identifiers 106,061 106,061 0
Symbols 371,546 371,546 0
Types 293,026 293,026 0
Instantiations 3,564,730 3,564,730 0
Memory used 229,697K 229,673K -24K
Memory allocs 9,996,685 9,996,700 15
Parse time 0.076s 0.076s 0s
Bind time 0.016s 0.016s 0s
Check time 1.413s 1.456s 0.043s
Emit time 0s 0s 0s
Total time 1.509s 1.549s 0.04s

Reported by octocov

Copy link

github-actions bot commented Aug 2, 2025

HTTP Performance Benchmark

Framework Runtime Average Ping Query Body
hono (origin/main) bun 38,570.24 53,261.48 33,606.99 28,842.25
hono (current) bun 38,752.53 52,635.38 34,357.57 29,264.63
Change +0.47% -1.18% +2.23% +1.46%

@@ -1,3 +1,4 @@
import { join } from 'node:path'
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use @std/path, but it's better to use node:path because it's built into Deno.

expect(await res.text()).toBe('Hello in ./static/sub/index.html')
})

it('Should return 200 response - /static/helloworld', async () => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is the same as Should return 200 response - /static/sub?? We may remove this.

@yusukebe
Copy link
Member Author

yusukebe commented Aug 2, 2025

Hi @usualoma Thank you for the accurate comment!

For Deno, as you said, we can use node:path's join. Updated: b3738f7

For the separators issue, I made defaultJoin support only Linux (posix), and it does not support Windows. This means if runtime-depends serve static-middleware (adapter/bun/serve-static.ts or adapter/deno/serve-static.ts) does not specify path option, it can't handle Windows paths. But, I think it's okay to cut Windows support because these runtime-dependent serve static-middleware can use join in the runtime API.

What do you think of this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants