Skip to content

fix: refuse request paths the URL parser would rewrite - #163

Merged
KyleJune merged 2 commits into
mainfrom
fix/refuse-unresolved-paths
Sep 25, 2026
Merged

KyleJune merged 2 commits into
mainfrom
fix/refuse-unresolved-paths

Conversation

@KyleJune

Copy link
Copy Markdown
Member

Summary

Hono matches the request path as the client sent it; React Router matches the resolved one. Deno.serve leaves .. in request.url, so GET /docs/../admin matched a /docs catch-all's middleware in Hono while React Router ran the /admin loader — /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.

createServer now refuses with 400, before any route middleware, any request whose raw path differs from new 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.com resolves to //evil.com).

Changes

  • src/server.tsx — isUnresolvedPath and the first appWrapper middleware; createServer JSDoc states the contract.
  • src/server.test.tsx — a suite that speaks raw HTTP over a socket (server.request() cannot reproduce this: building a Request resolves 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 — 200 with the private page rendered and runs.guard at 0:

AssertionError: Values are not equal: Hono matched the raw path under /docs while React Router resolved it to /admin, so the guard on /admin never ran: 200 ...<div>Private admin</div>..."loaderData":{"/admin":{"secret":true}}
-   200
+   400

Reversing only the src/server.tsx hunk reproduces the same failure. deno task check and deno task test --parallel --reporter=dot (60 passed, 851 steps) are green from packages/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

KyleJune and others added 2 commits September 25, 2026 00:22
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>
@KyleJune

Copy link
Copy Markdown
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
KyleJune merged commit cc4b1ad into main Sep 25, 2026
10 checks passed
@KyleJune
KyleJune deleted the fix/refuse-unresolved-paths branch September 25, 2026 05:19
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))
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 0.17.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant