Skip to content

Commit 459a1c6

Browse files
committed
fix(utils): prevent open redirect via protocol-relative path in redirectBack()
sanitize referer pathname starting with `//` to avoid browser interpreting it as protocol-relative URL
1 parent 388b8f0 commit 459a1c6

File tree

2 files changed

+16
-1
lines changed

2 files changed

+16
-1
lines changed

src/utils/response.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,11 @@ export function redirectBack(
9292
if (referer && URL.canParse(referer)) {
9393
const refererURL = new URL(referer);
9494
if (refererURL.origin === event.url.origin) {
95-
location = refererURL.pathname + (opts.allowQuery ? refererURL.search : "");
95+
let pathname = refererURL.pathname;
96+
if (pathname.startsWith("//")) {
97+
pathname = "/" + pathname.replace(/^\/+/, "");
98+
}
99+
location = pathname + (opts.allowQuery ? refererURL.search : "");
96100
}
97101
}
98102
return redirect(location, opts.status);

test/utils.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,17 @@ describeMatrix("utils", (t, { it, describe, expect }) => {
109109
expect(res.headers.get("location")).toBe("/");
110110
});
111111

112+
it("prevents open redirect via protocol-relative path in referer", async () => {
113+
t.app.post("/submit", (event) => redirectBack(event));
114+
const baseUrl = t.url || "http://localhost";
115+
const res = await t.fetch("/submit", {
116+
method: "POST",
117+
headers: { referer: `${baseUrl}//evil.com/steal` },
118+
});
119+
expect(res.headers.get("location")).not.toBe("//evil.com/steal");
120+
expect(res.headers.get("location")).toBe("/evil.com/steal");
121+
});
122+
112123
it("uses fallback when referer is invalid URL", async () => {
113124
t.app.post("/submit", (event) => redirectBack(event, { fallback: "/safe" }));
114125
const res = await t.fetch("/submit", {

0 commit comments

Comments
 (0)