fix: refuse request paths the URL parser would rewrite - #163
Merged
Merged
Conversation
Hono routes on the path as the client sent it while React Router matches the resolved one, so GET /docs/../admin matched a /docs catch-all's middleware in Hono and ran the /admin loader in React Router, skipping /admin's guard. createServer now answers 400 before any route middleware whenever the raw path differs from new URL(url).pathname: dot segments, raw or percent-encoded, backslashes and fragments. Browsers resolve those before sending, so only a hand-built request is refused. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The raw-versus-parsed comparison refuses more than dot segments: raw UTF-8 and other bytes the URL parser would percent-encode, and a Host carrying a slash, a question mark or a backslash. The createServer JSDoc and the middleware guide now say so, fail closed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Member
Author
|
docs: state the full set the path check refuses — 2f54ef0. Review fix: createServer JSDoc, isUnresolvedPath JSDoc and docs/middleware.md now name the wider fail-closed set (raw UTF-8 and other bytes the parser percent-encodes, a Host carrying / ? or ). No unit test exists for the private predicate here, so the /café pin lives in udibo's utils/request-path.test.ts. Gates: check clean, 60 passed (851 steps). |
KyleJune
pushed a commit
that referenced
this pull request
Sep 25, 2026
## [0.17.1](0.17.0...0.17.1) (2026-09-25) ### Bug Fixes * refuse request paths the URL parser would rewrite ([#163](#163)) ([cc4b1ad](cc4b1ad))
|
🎉 This PR is included in version 0.17.1 🎉 The release is available on: Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Hono matches the request path as the client sent it; React Router matches the resolved one.
Deno.serveleaves..inrequest.url, soGET /docs/../adminmatched a/docscatch-all's middleware in Hono while React Router ran the/adminloader —/admin's own Hono guard never ran. Percent-encoded (%2e%2e,.%2e), single-dot and backslash forms resolve the same way. Found as udibo/udibo#1531.createServernow refuses with 400, before any route middleware, any request whose raw path differs fromnew URL(request.url).pathname. Refusal rather than a 308 to the resolved path: browsers already resolve dot segments before sending, so only a hand-built request is affected, and a redirect built from the resolved pathname would need its own guard against//host(/docs/..//evil.comresolves to//evil.com).Changes
src/server.tsx—isUnresolvedPathand the firstappWrappermiddleware;createServerJSDoc states the contract.src/server.test.tsx— a suite that speaks raw HTTP over a socket (server.request()cannot reproduce this: building aRequestresolves the path first). Eight rewritten forms must answer 400 with no guard, loader or catch-all run; a data request the same; the resolved path still gets its guard's 403;/docs/a%20b,v1..v2, and dot segments in the query still serve.docs/middleware.md— a paragraph under Middleware Order on why path-specific middleware would otherwise be skipped.Testing
Before the fix, the eight refusal steps and the data-request step failed at the intended assertion —
200with the private page rendered andruns.guardat 0:Reversing only the
src/server.tsxhunk reproduces the same failure.deno task checkanddeno task test --parallel --reporter=dot(60 passed, 851 steps) are green frompackages/juniper.Closes
Nothing here. udibo/udibo#1531 stays open until the udibo PR merges; that PR carries an interim root middleware until this release is adopted.
🤖 Generated with Claude Code