diff --git a/console/src/lib/validate-redirect.ts b/console/src/lib/validate-redirect.ts new file mode 100644 index 000000000..e86a4f050 --- /dev/null +++ b/console/src/lib/validate-redirect.ts @@ -0,0 +1,20 @@ +export function validateRedirect(url: string | null | undefined): string { + if (!url) return "/" + + const candidate = url + if (!candidate || candidate.startsWith("//") || candidate.startsWith("\\\\")) { + return "/" + } + + try { + const parsed = new URL(candidate, window.location.origin) + + if (parsed.origin !== window.location.origin) { + return "/" + } + + return `${parsed.pathname}${parsed.search}${parsed.hash}` + } catch { + return "/" + } +} diff --git a/console/src/views/auth/Login.tsx b/console/src/views/auth/Login.tsx index 657229aee..3ac49954c 100644 --- a/console/src/views/auth/Login.tsx +++ b/console/src/views/auth/Login.tsx @@ -7,6 +7,7 @@ import { useForm } from "react-hook-form" import api from "../../api" import { type AuthDriver, AUTH_DRIVERS } from "../../types" +import { validateRedirect } from "@/lib/validate-redirect" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" @@ -35,7 +36,7 @@ export default function Login() { const [selectedDriver, setSelectedDriver] = useState() const [error, setError] = useState() const [isSubmitting, setIsSubmitting] = useState(false) - const redirect = searchParams.get("r") ?? "/" + const redirect = validateRedirect(searchParams.get("r")) const form = useForm({ defaultValues: { @@ -227,4 +228,4 @@ export default function Login() { ) -} +} \ No newline at end of file diff --git a/console/src/views/auth/LoginCallback.tsx b/console/src/views/auth/LoginCallback.tsx index 640899cf7..d28279c96 100644 --- a/console/src/views/auth/LoginCallback.tsx +++ b/console/src/views/auth/LoginCallback.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from "react" import { useClerk } from "@clerk/clerk-react" import api from "../../api" import { AUTH_DRIVERS } from "../../types" +import { validateRedirect } from "@/lib/validate-redirect" import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert" import { useTranslation } from "react-i18next" @@ -14,7 +15,7 @@ export default function LoginCallback() { const { driver } = useParams() as { driver: string } const [searchParams] = useSearchParams() const [error, setError] = useState() - const redirect = searchParams.get("r") ?? "/" + const redirect = validateRedirect(searchParams.get("r")) useEffect(() => { const handleAuth = async () => { diff --git a/internal/config/config.go b/internal/config/config.go index 0cbfadba6..cd31c9427 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -39,12 +39,13 @@ func (n Node) PublicBaseURL() string { } type Auth struct { - Driver string `env:"DRIVER"` - JWTSecret string `env:"JWT_SECRET"` - JWKS claim.JWKS `env:"JWKS_URL"` - TokenLife time.Duration `env:"TOKEN_LIFE" envDefault:"24h"` - Basic BasicAuth `envPrefix:"BASIC_"` - Clerk ClerkAuth `envPrefix:"CLERK_"` + Driver string `env:"DRIVER"` + JWTSecret string `env:"JWT_SECRET"` + JWKS claim.JWKS `env:"JWKS_URL"` + TokenLife time.Duration `env:"TOKEN_LIFE" envDefault:"24h"` + AllowedRedirectHosts []string `env:"ALLOWED_REDIRECT_HOSTS"` + Basic BasicAuth `envPrefix:"BASIC_"` + Clerk ClerkAuth `envPrefix:"CLERK_"` } type BasicAuth struct { diff --git a/internal/http/controllers/v1/management/auth.go b/internal/http/controllers/v1/management/auth.go index 795a58a5e..8de7da240 100644 --- a/internal/http/controllers/v1/management/auth.go +++ b/internal/http/controllers/v1/management/auth.go @@ -86,4 +86,4 @@ func (c *AuthController) writeAuthError(w http.ResponseWriter, err error) { default: oapi.WriteProblem(w, problem.ErrInternal(problem.Describe("authentication failed"))) } -} +} \ No newline at end of file diff --git a/internal/http/controllers/v1/management/auth_test.go b/internal/http/controllers/v1/management/auth_test.go index 5b4ab890e..3b2fe0c62 100644 --- a/internal/http/controllers/v1/management/auth_test.go +++ b/internal/http/controllers/v1/management/auth_test.go @@ -139,4 +139,4 @@ func TestAuthWebhookWithInvalidDriver(t *testing.T) { require.Equal(t, test.code, res.Code, res.Body.String()) }) } -} +} \ No newline at end of file diff --git a/internal/http/controllers/v1/management/oapi/resources.yml b/internal/http/controllers/v1/management/oapi/resources.yml index 36516e729..91d2c78b2 100644 --- a/internal/http/controllers/v1/management/oapi/resources.yml +++ b/internal/http/controllers/v1/management/oapi/resources.yml @@ -8417,10 +8417,6 @@ components: type: string description: Password (required for basic auth) example: "password123" - redirect: - type: string - description: URL to redirect after successful auth - example: "/" EmailTemplate: type: object