From b3e9b3351b7bbff98eef21e68a03051f0f9836ae Mon Sep 17 00:00:00 2001 From: Manuel <30572287+manuel-rw@users.noreply.github.com> Date: Fri, 27 Mar 2026 17:50:26 +0100 Subject: [PATCH] Merge commit from fork --- .../src/app/[locale]/auth/login/_login-form.tsx | 9 ++++++++- apps/nextjs/src/app/[locale]/auth/login/page.tsx | 3 ++- packages/validation/src/redirection-url.ts | 14 ++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 packages/validation/src/redirection-url.ts diff --git a/apps/nextjs/src/app/[locale]/auth/login/_login-form.tsx b/apps/nextjs/src/app/[locale]/auth/login/_login-form.tsx index 411055756..ef4dc2aeb 100644 --- a/apps/nextjs/src/app/[locale]/auth/login/_login-form.tsx +++ b/apps/nextjs/src/app/[locale]/auth/login/_login-form.tsx @@ -7,14 +7,21 @@ import { Anchor, Button, Card, Code, Collapse, Divider, PasswordInput, Stack, Te import { useDisclosure } from "@mantine/hooks"; import { z } from "zod/v4"; + + import { signIn } from "@homarr/auth/client"; import { revalidatePathActionAsync } from "@homarr/common/client"; import type { useForm } from "@homarr/form"; import { useZodForm } from "@homarr/form"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { useScopedI18n } from "@homarr/translation/client"; +import { sanitizeRedirectionUrl } from "@homarr/validation/redirection-url"; import { userSignInSchema } from "@homarr/validation/user"; + + + + type Provider = "credentials" | "ldap" | "oidc"; interface LoginFormProps { @@ -70,7 +77,7 @@ export const LoginForm = ({ providers, oidcClientName, isOidcAutoLoginEnabled, c // Redirect to the callback URL if the response is defined and comes from a credentials provider (ldap or credentials). oidc is redirected automatically. await revalidatePathActionAsync("/"); - router.push(callbackUrl); + router.push(sanitizeRedirectionUrl(callbackUrl)); }, [t, router, callbackUrl], ); diff --git a/apps/nextjs/src/app/[locale]/auth/login/page.tsx b/apps/nextjs/src/app/[locale]/auth/login/page.tsx index c9d913c08..1dca9baaf 100644 --- a/apps/nextjs/src/app/[locale]/auth/login/page.tsx +++ b/apps/nextjs/src/app/[locale]/auth/login/page.tsx @@ -7,6 +7,7 @@ import { getScopedI18n } from "@homarr/translation/server"; import { HomarrLogoWithTitle } from "~/components/layout/logo/homarr-logo"; import { LoginForm } from "./_login-form"; +import { sanitizeRedirectionUrl } from "@homarr/validation/redirection-url"; interface LoginProps { searchParams: Promise<{ @@ -19,7 +20,7 @@ export default async function Login(props: LoginProps) { const session = await auth(); if (session) { - redirect(searchParams.callbackUrl ?? "/"); + redirect(sanitizeRedirectionUrl(searchParams.callbackUrl)); } const t = await getScopedI18n("user.page.login"); diff --git a/packages/validation/src/redirection-url.ts b/packages/validation/src/redirection-url.ts new file mode 100644 index 000000000..e9de8af69 --- /dev/null +++ b/packages/validation/src/redirection-url.ts @@ -0,0 +1,14 @@ +/** + * Sanitizes the URL from malicious payloads. + * Prevents open redirects such as //example.com/path + * and prevents XSS such as javascript:(). + * Returns / as a default or when unsafe. + * @param url The URL to sanitize + */ +export const sanitizeRedirectionUrl = (url?: string) => { + if (url && url.startsWith("/") && !url.startsWith("//")) { + return url; + } else { + return '/'; // Safe default + } +} \ No newline at end of file