Skip to content

Commit 64acfe6

Browse files
committed
fix(withoutBase): collapse leading slashes
Slicing the base prefix from inputs like `/api//evil.com` left the remainder as `//evil.com`, which browsers interpret as a protocol-relative URL and could enable open redirects when the result flows into a `Location` header (via `withBase` → `event.url.pathname`). Normalize all leading slashes on the trimmed remainder so the output is always a single-host-relative path. Ref: unjs/ufo#335
1 parent d77b673 commit 64acfe6

2 files changed

Lines changed: 10 additions & 2 deletions

File tree

src/utils/internal/path.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@ export function withoutBase(input: string = "", base: string = ""): string {
4040
if (!input.startsWith(_base) || (input.length > _base.length && input[_base.length] !== "/")) {
4141
return input;
4242
}
43-
const trimmed = input.slice(_base.length);
44-
return trimmed[0] === "/" ? trimmed : "/" + trimmed;
43+
// Collapse leading slashes to prevent protocol-relative URL injection
44+
// e.g. withoutBase("/legacy//evil.com", "/legacy") must not return "//evil.com"
45+
const trimmed = input.slice(_base.length).replace(/^\/+/, "");
46+
return "/" + trimmed;
4547
}
4648

4749
export function getPathname(path: string = "/"): string {

test/utils.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,12 @@ describeMatrix("utils", (t, { it, describe, expect }) => {
137137

138138
expect(await result.text()).toBe("/api/test");
139139
});
140+
it("collapses leading slashes after stripping base", async () => {
141+
t.app.use(withBase("/api", (event) => Promise.resolve(event.path)));
142+
const result = await t.fetch("/api//evil.com");
143+
144+
expect(await result.text()).toBe("/evil.com");
145+
});
140146
});
141147

142148
describe("getQuery", () => {

0 commit comments

Comments
 (0)