Skip to content

Commit 7ccc9e2

Browse files
committed
fix(mount): enforce path segment boundary in startsWith check
Prevent mounted middleware from executing on unrelated routes that share a string prefix (e.g., `/admin-public` matching `/admin` mount).
1 parent 8e9993f commit 7ccc9e2

File tree

3 files changed

+29
-2
lines changed

3 files changed

+29
-2
lines changed

src/h3.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,10 @@ export const H3 = /* @__PURE__ */ (() => {
124124
if (input["~middleware"].length > 0) {
125125
this["~middleware"].push((event, next) => {
126126
const originalPathname = event.url.pathname;
127-
if (!originalPathname.startsWith(base)) {
127+
if (
128+
!originalPathname.startsWith(base) ||
129+
(originalPathname.length > base.length && originalPathname[base.length] !== "/")
130+
) {
128131
return next();
129132
}
130133
event.url.pathname = event.url.pathname.slice(base.length) || "/";

src/utils/internal/path.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export function withoutBase(input: string = "", base: string = ""): string {
3737
return input;
3838
}
3939
const _base = withoutTrailingSlash(base);
40-
if (!input.startsWith(_base)) {
40+
if (!input.startsWith(_base) || (input.length > _base.length && input[_base.length] !== "/")) {
4141
return input;
4242
}
4343
const trimmed = input.slice(_base.length);

test/mount.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,5 +197,29 @@ describeMatrix("mount", (t, { it, expect, describe }) => {
197197

198198
expect(logs).toHaveLength(0); // Middleware should not execute
199199
});
200+
201+
it("mounted middleware should not execute for prefix-matching paths without segment boundary", async () => {
202+
const adminApp = new H3();
203+
adminApp.use((event, next) => {
204+
event.context.isAdmin = true;
205+
return next();
206+
});
207+
adminApp.get("/dashboard", () => ({ admin: true }));
208+
209+
t.app.mount("/admin", adminApp);
210+
t.app.get("/admin-public/info", (event) => ({
211+
path: event.url.pathname,
212+
isAdmin: event.context.isAdmin ?? false,
213+
}));
214+
215+
// /admin/dashboard should trigger admin middleware
216+
const adminRes = await t.fetch("/admin/dashboard");
217+
expect(adminRes.status).toBe(200);
218+
219+
// /admin-public/info should NOT trigger admin middleware
220+
const publicRes = await t.fetch("/admin-public/info");
221+
const body = await publicRes.json();
222+
expect(body.isAdmin).toBe(false);
223+
});
200224
});
201225
});

0 commit comments

Comments
 (0)