GitHub Advisoryadvisory · npm
GHSA-2j6q-whv2-gh6w h3: Missing Path Segment Boundary Check in `mount()` Causes Middleware Execution on Unrelated Prefix-Matching Routes
LOW
Official page ↗
- Aliases
- CVE-2026-33490
- Ecosystem
- npm
- Packages
- h3
- Severity
- Low
- CVSS vector
- CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:N
- Weaknesses
- CWE-706
- Reviewed
- 2026-03-20
Affected packages
• npm h3: >= 2.0.1-alpha.0, < 2.0.1-rc.17 → fixed in 2.0.1-rc.17
Description
Summary
The `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.
Details
The root cause is in `src/h3.ts:127` within the `mount()` method:
```typescript
// src/h3.ts:122-135
mount(base: string, input: FetchHandler | FetchableObject | H3Type) {
if ("handler" in input) {
if (input["~middleware"].length > 0) {
this["~middleware"].push((event, next) => {
const originalPathname = event.url.pathname;
if (!originalPathname.startsWith(base)) { // {
event.url.pathname = originalPathname;
return next();
});
});
}
```
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`:
```typescript
// src/utils/internal/path.ts:35-45
export function withoutBase(input: string = "", base: string = ""): string {
if (!base || base === "/") {
return input;
}
const _base = withoutTrailingSlash(base);
if (!input.startsWith(_base)) { // {}, {
onRequest: (event) => {
event.context.isAdmin = true;
}
});
adminApp.get("/dashboard", (event) => {
return { admin: true, context: event.context };
});
const app = new H3();
// Mount admin sub-app at /admin
app.mount("/admin", adminApp);
// Public route that happens to share the "/admin" prefix
app.get("/admin-public/info", (event) => {
return {
path: event.url.pathname,
isAdmin: event.context.isAdmin ?? false, // Should always be false here
};
});
// Test with fetch
const server = Bun.serve({ port: 3000, fetch: app.fetch });
// This request should NOT trigger admin middleware, but it does
const res = await fetch("http://localhost:3000/admin-public/info");
const body = await res.json();
console.log(body);
// Actual output: { path: "/admin-public/info", isAdmin: true }
// Expected output: { path: "/admin-public/info", isAdmin: false }
server.stop();
```
Steps to reproduce:
```bash
1. Clone h3 and install
git clone https://github.com/h3js/h3 && cd h3
corepack enable && pnpm install && pnpm build
2. Save poc.js (above) and run
bun poc.js
Output shows isAdmin: true — admin middleware leaked to /admin-public/info
3. Verify the boundary leak with additional paths:
GET /administrator → admin middleware fires
GET /adminstuff → admin middleware fires
GET /admin123 → admin middleware fires
GET /admi → admin middleware does NOT fire (correct)
```
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 Fix
Add 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`:
```diff
mount(base: string, input: FetchHandler | FetchableObject | H3Type) {
if ("handler" in input) {
if (input["~middleware"].length > 0) {
this["~middleware"].push((event, next) => {
const originalPathname = event.url.pathname;
• if (!originalPathname.startsWith(base)) {
+ if (!originalPathname.startsWith(base) ||
+ (originalPathname.length > base.length && originalPathname[base.length] !== "/")) {
return next();
}
```
Fix for `src/utils/internal/path.ts:40`:
```diff
export function withoutBase(input: string = "", base: string = ""): string {
if (!base || base === "/") {
return input;
}
const _base = withoutTrailingSlash(base);
• if (!input.startsWith(_base)) {
+ if (!input.startsWith(_base) ||
+ (input.length > _base.length && input[_base.length] !== "/")) {
return input;
}
```
This ensures that `/admin` only matches `/admin`, `/admin/`, and `/admin/...` — never `/admin-public`, `/administrator`, or other coincidental string-prefix matches.
Related
Alias · CVE (1)
- CVE-2026-33490 H3 is a minimal H(TTP) framework. vulnerability
Weakness · CWE (1)
- CWE-706 Use of Incorrectly-Resolved Name or Reference weakness