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 {