diff --git a/app/login/page.tsx b/app/login/page.tsx index 5416abd..45d1716 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -6,6 +6,16 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com export const dynamic = "force-dynamic"; +// Only allow redirecting back to a same-origin relative path. Anything else +// (absolute URLs, protocol-relative `//evil.com`, backslashes, etc.) falls +// back to `/` to prevent open-redirect abuse. +function safeNext(next: string | undefined | null): string { + if (!next) return "/"; + if (!next.startsWith("/")) return "/"; + if (next.startsWith("//") || next.startsWith("/\\")) return "/"; + return next; +} + async function doLogin(formData: FormData) { "use server"; const password = String(formData.get("password") ?? ""); @@ -13,8 +23,8 @@ async function doLogin(formData: FormData) { if (!ok) { redirect("/login?error=1"); } - const next = String(formData.get("next") ?? "/"); - redirect(next || "/"); + const next = safeNext(String(formData.get("next") ?? "/")); + redirect(next); } export default async function LoginPage({ @@ -23,8 +33,9 @@ export default async function LoginPage({ searchParams: Promise<{ error?: string; next?: string }>; }) { const params = await searchParams; + const next = safeNext(params.next); if (await isAuthed()) { - redirect(params.next || "/"); + redirect(next); } return (
@@ -35,7 +46,7 @@ export default async function LoginPage({
- + ) { return env.APP_SESSION_SECRET || env.APP_PASSWORD || "dev-insecure-secret"; } @@ -37,8 +47,12 @@ export async function verify(signed: string | undefined, secret: string) { const idx = signed.lastIndexOf("."); if (idx < 0) return false; const value = signed.slice(0, idx); - const expected = await sign(value, secret); - return expected === signed; + const sigHex = signed.slice(idx + 1); + const sig = fromHex(sigHex); + if (!sig) return false; + const key = await hmacKey(secret); + // crypto.subtle.verify performs a timing-safe comparison internally. + return crypto.subtle.verify("HMAC", key, sig, enc.encode(value)); } export const SESSION_COOKIE = "kilo_control_session";