GHSA-2j6q-whv2-gh6w: h3: Missing Path Segment Boundary Check in `mount()` Causes Middleware Execution on Unrelated Prefix-Matching Routes
SummaryThe `mount()` method in h3 uses a simple `startsWith()` check to determine whether incoming requests fall under a mounted sub-application's path prefix. Because this check does not verify a path segment boundary (i.e., that the next character after the base is `/` or end-of-string), middleware registered on a mount like `/admin` will also execute for unrelated routes such as `/admin-public`, `/administrator`, or `/adminstuff`. This allows an attacker to trigger context-setting middleware on paths it was never intended to cover, potentially polluting request context with unintended privilege flags.DetailsThe root cause is in `src/h3.ts:127` within the `mount()` method:When a sub-app is mounted at `/admin`, the check `originalPathname.startsWith("/admin")` returns `true` for `/admin`, `/admin/`, `/admin/dashboard`, but also for `/admin-public`, `/administrator`, `/adminFoo`, etc. The mounted sub-app's entire middleware chain then executes for these unrelated paths.A secondary instance of the same flaw exists in `src/utils/internal/path.ts:40`:The `withoutBase()` utility will incorrectly strip the base from paths that merely share a string prefix, returning mangled paths (e.g., `withoutBase("/admin-public/info", "/admin")` returns `/-public/info`).**Exploitation flow:**Developer mounts a sub-app at `/admin` with middleware that sets `event.context.isAdmin = true`Developer defines a separate route `/admin-public/info` on the parent app that reads `event.context.isAdmin`Attacker requests `GET /admin-public/info`The `/admin` mount's `startsWith` check passes → admin middleware executes → sets `isAdmin = true`The middleware's "restore pathname" callback fires, control returns to the parent appThe `/admin-public/info` handler sees `event.context.isAdmin === true`PoC**Steps to reproduce:**Impact**Context pollution across mount boundaries**: Middleware registered on a mounted sub-app executes for any route sharing the string prefix, not just routes under the intended path segment tree. This can set privileged flags (`isAdmin`, `isAuthenticated`, role assignments) on requests to completely unrelated routes.**Authorization bypass**: If an application uses mount-scoped middleware to set permissive context flags and other routes check those flags, an attacker can access protected functionality by requesting a path that string-prefix-matches the mount base but routes to a different handler.**Path mangling**: The `withoutBase()` utility produces incorrect paths (e.g., `/-public/info` instead of `/admin-public/info`) when the input shares only a string prefix, potentially causing routing errors or further security issues in downstream path processing.**Scope**: Any h3 v2 application using `mount()` with a base path that is a string prefix of other routes is affected. The impact scales with how the application uses middleware-set context values.Recommended FixAdd a segment boundary check after the `startsWith` call in both locations. The character immediately following the base prefix must be `/`, `?`, `#`, or the string must end exactly at the base:**Fix for `src/h3.ts:127`:****Fix for `src/utils/internal/path.ts:40`:**This ensures that `/admin` only matches `/admin`, `/admin/`, and `/admin/...` — never `/admin-public`, `/administrator`, or other coincidental string-prefix matches.
CVSS v3.1: 3.7