From 9c679d0ab97b3b0e9996198415f1f2fc3b86d512 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 23 Jul 2026 21:48:41 -0400 Subject: [PATCH] M9 S5: guard the login ?redirect= against open redirect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LoginView handed route.query.redirect straight to router.replace, so a crafted link like /login?redirect=//evil.com (or a backslash variant) could bounce a just-authenticated user off-site. safeRedirect() now only follows an in-app absolute path — a single leading slash, rejecting "//host" and "/\\host" (and anything without a leading slash, i.e. absolute/scheme URLs) → falls back to "/". Frontend-only; CI vue-tsc is the gate. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm --- frontend/src/views/LoginView.vue | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/LoginView.vue b/frontend/src/views/LoginView.vue index 60f9697..5a58338 100644 --- a/frontend/src/views/LoginView.vue +++ b/frontend/src/views/LoginView.vue @@ -17,13 +17,21 @@ const password = ref(""); const error = ref(""); const loading = ref(false); +// Only follow an in-app absolute path from ?redirect= — reject protocol-relative +// ("//host") and backslash ("/\\host") forms a browser may treat as an off-site URL, +// so a crafted login link can't bounce the user elsewhere after they sign in. +function safeRedirect(raw: unknown): string { + if (typeof raw !== "string" || !raw.startsWith("/")) return "/"; + if (raw.startsWith("//") || raw.startsWith("/\\")) return "/"; + return raw; +} + async function submit() { error.value = ""; loading.value = true; try { await session.login(email.value, password.value); - const redirect = typeof route.query.redirect === "string" ? route.query.redirect : "/"; - await router.replace(redirect); + await router.replace(safeRedirect(route.query.redirect)); } catch (e) { error.value = (e as ApiError).error ?? "Could not sign in."; } finally {