M9 S5: guard the login ?redirect= against open redirect
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / Python tests (push) Successful in 8s
CI & Build / Build & push image (push) Successful in 30s

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
This commit is contained in:
2026-07-23 21:48:41 -04:00
co-authored by Claude Opus 4.8
parent be34fe8619
commit 9c679d0ab9
+10 -2
View File
@@ -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 {