24 lines · 1.1 KB
const MAX_RETURN_PATH = 2048;
const SIGN_IN_PATHS = ["/login", "/signup", "/logout", "/forgot-password", "/reset-password"];

/**
 * A same-origin path to return to after signing in, or null. Only plain absolute paths are accepted, so a
 * `next` value can never send the user to another site (`//evil`, `/\evil`, `https:…`, control characters).
 */
export function safeReturnPath(raw: unknown): string | null {
  if (typeof raw !== "string" || raw.length === 0 || raw.length > MAX_RETURN_PATH) return null;
  if (!raw.startsWith("/") || raw.startsWith("//") || raw.startsWith("/\\")) return null;
  if (/[\u0000-\u001f\u007f\\]/.test(raw)) return null;
  const path = raw.split(/[?#]/, 1)[0] ?? "";
  if (SIGN_IN_PATHS.some((prefix) => path === prefix || path.startsWith(`${prefix}/`))) return null;
  return raw;
}

export type ReturnSearch = { next?: string };

/** `validateSearch` for sign-in pages: keeps `next` only when it's a safe path. */
export function returnSearch(search: Record<string, unknown>): ReturnSearch {
  const next = safeReturnPath(search.next);
  return next ? { next } : {};
}