Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,25 @@ 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") ?? "");
const ok = await login(password);
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({
Expand All @@ -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 (
<div className="flex min-h-screen items-center justify-center p-6">
Expand All @@ -35,7 +46,7 @@ export default async function LoginPage({
</CardHeader>
<CardContent>
<form action={doLogin} className="space-y-3">
<input type="hidden" name="next" value={params.next ?? "/"} />
<input type="hidden" name="next" value={next} />
<Input
name="password"
type="password"
Expand Down
18 changes: 16 additions & 2 deletions lib/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ function toHex(buf: ArrayBuffer) {
return out;
}

function fromHex(hex: string) {
if (hex.length === 0 || hex.length % 2 !== 0) return null;
if (!/^[0-9a-fA-F]+$/.test(hex)) return null;
const out = new Uint8Array(hex.length / 2);
for (let i = 0; i < out.length; i++) {
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
}
return out;
}

export function sessionSecret(env: Record<string, string | undefined>) {
return env.APP_SESSION_SECRET || env.APP_PASSWORD || "dev-insecure-secret";
}
Expand All @@ -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";