Skip to content

Commit aeca15e

Browse files
fix: open redirect issue with trailing slash detection (#2313)
Passing a path like `/foo/bar` as the location header is interpreted as a relative path. But when it contains two leading slashes like `//evil.com/` it is interpreted as a port relative url. This has the consequence that the origin of a URL can essentially be replaced: ```ts new URL("//evil.com", "https://example.com/"); // -> 'https://evil.com/' ``` Our trailing slash detection logic didn't guard against this case. By normalizing the pathname of the URL and stripping sibling forward slashes we sidestep this problem.
1 parent d19c22d commit aeca15e

File tree

2 files changed

+18
-4
lines changed

2 files changed

+18
-4
lines changed

src/server/context.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,11 @@ export class ServerContext {
187187
connInfo: ServeHandlerInfo = DEFAULT_CONN_INFO,
188188
) {
189189
const url = new URL(req.url);
190+
// Syntactically having double slashes in the pathname is valid per
191+
// spec, but there is no behavior defined for that. Practically all
192+
// servers normalize the pathname of a URL to not include double
193+
// forward slashes.
194+
url.pathname = url.pathname.replaceAll(/\/+/g, "/");
190195

191196
const aliveUrl = basePath + ALIVE_URL;
192197

tests/main_test.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -232,27 +232,27 @@ Deno.test("redirect /pages/fresh/ to /pages/fresh", async () => {
232232
);
233233
});
234234

235-
Deno.test("redirect /pages/////fresh///// to /pages/////fresh", async () => {
235+
Deno.test("redirect /pages/////fresh///// to /pages/fresh", async () => {
236236
const resp = await handler(
237237
new Request("https://fresh.deno.dev/pages/////fresh/////"),
238238
);
239239
assert(resp);
240240
assertEquals(resp.status, STATUS_CODE.TemporaryRedirect);
241241
assertEquals(
242242
resp.headers.get("location"),
243-
"/pages/////fresh",
243+
"/pages/fresh",
244244
);
245245
});
246246

247-
Deno.test("redirect /pages/////fresh/ to /pages/////fresh", async () => {
247+
Deno.test("redirect /pages/////fresh/ to /pages/fresh", async () => {
248248
const resp = await handler(
249249
new Request("https://fresh.deno.dev/pages/////fresh/"),
250250
);
251251
assert(resp);
252252
assertEquals(resp.status, STATUS_CODE.TemporaryRedirect);
253253
assertEquals(
254254
resp.headers.get("location"),
255-
"/pages/////fresh",
255+
"/pages/fresh",
256256
);
257257
});
258258

@@ -264,6 +264,15 @@ Deno.test("no redirect for /pages/////fresh", async () => {
264264
assertEquals(resp.status, STATUS_CODE.NotFound);
265265
});
266266

267+
Deno.test("no open redirect when passing double slashes", async () => {
268+
const resp = await handler(
269+
new Request("https://fresh.deno.dev//evil.com/"),
270+
);
271+
assert(resp);
272+
assertEquals(resp.status, STATUS_CODE.TemporaryRedirect);
273+
assertEquals(resp.headers.get("location"), "/evil.com");
274+
});
275+
267276
Deno.test("/failure", async () => {
268277
const resp = await handler(new Request("https://fresh.deno.dev/failure"));
269278
assert(resp);

0 commit comments

Comments
 (0)