diff --git a/web/src/app/login/login-form.tsx b/web/src/app/login/login-form.tsx index 7f12e1b..a7134a4 100644 --- a/web/src/app/login/login-form.tsx +++ b/web/src/app/login/login-form.tsx @@ -1,12 +1,14 @@ "use client"; import { useState } from "react"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useSearchParams } from "next/navigation"; export function LoginForm() { - const router = useRouter(); const params = useSearchParams(); - const next = params.get("next") || "/"; + // Only allow same-origin relative paths, so a crafted ?next=https://evil or ?next=//evil + // can't turn login into an open redirect. + const raw = params.get("next") || "/"; + const next = raw.startsWith("/") && !raw.startsWith("//") ? raw : "/"; const [password, setPassword] = useState(""); const [error, setError] = useState(""); const [busy, setBusy] = useState(false); diff --git a/web/src/lib/auth.ts b/web/src/lib/auth.ts index d43e733..edeb2b6 100644 --- a/web/src/lib/auth.ts +++ b/web/src/lib/auth.ts @@ -13,11 +13,16 @@ function toB64(bytes: Uint8Array): string { return btoa(s); } +// Memoize the derived token so the middleware doesn't recompute an HMAC on every request +// (password/secret don't change at runtime; keyed on both so a change still recomputes). +let _cache: { password: string; secret: string; token: string } | null = null; + /** The token a valid session cookie must carry, or "" when auth is disabled (no password). */ export async function expectedToken(): Promise { const password = process.env.LYRA_PASSWORD; if (!password) return ""; const secret = process.env.LYRA_SECRET_KEY ?? "lyra"; + if (_cache && _cache.password === password && _cache.secret === secret) return _cache.token; const enc = new TextEncoder(); const key = await crypto.subtle.importKey( "raw", @@ -27,7 +32,9 @@ export async function expectedToken(): Promise { ["sign"], ); const sig = await crypto.subtle.sign("HMAC", key, enc.encode(password)); - return toB64(new Uint8Array(sig)); + const token = toB64(new Uint8Array(sig)); + _cache = { password, secret, token }; + return token; } /** Constant-time string compare (both operands are our own derived tokens). */