From c951d8ea95cd541df60ac53ed45f8dc8e4b32b5f Mon Sep 17 00:00:00 2001 From: vamsikrishnamathala Date: Wed, 17 Sep 2025 17:33:07 +0530 Subject: [PATCH 1/3] fix: next path url redirection --- apps/space/app/page.tsx | 25 +++++++++++++++++++++++-- packages/utils/src/url.ts | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/apps/space/app/page.tsx b/apps/space/app/page.tsx index a75275e0d59..f544bcb103e 100644 --- a/apps/space/app/page.tsx +++ b/apps/space/app/page.tsx @@ -1,6 +1,9 @@ "use client"; - +import { useEffect } from "react"; import { observer } from "mobx-react"; +import { useSearchParams, useRouter } from "next/navigation"; +// plane imports +import { isValidNextPath } from "@plane/utils"; // components import { UserLoggedIn } from "@/components/account/user-logged-in"; import { LogoSpinner } from "@/components/common/logo-spinner"; @@ -10,6 +13,15 @@ import { useUser } from "@/hooks/store/use-user"; const HomePage = observer(() => { const { data: currentUser, isAuthenticated, isInitializing } = useUser(); + const searchParams = useSearchParams(); + const router = useRouter(); + const nextPath = searchParams.get("next_path"); + + useEffect(() => { + if (currentUser && isAuthenticated && nextPath && isValidNextPath(nextPath)) { + router.replace(nextPath); + } + }, [currentUser, isAuthenticated, nextPath, router]); if (isInitializing) return ( @@ -18,7 +30,16 @@ const HomePage = observer(() => { ); - if (currentUser && isAuthenticated) return ; + if (currentUser && isAuthenticated) { + if (nextPath && isValidNextPath(nextPath)) { + return ( +
+ +
+ ); + } + return ; + } return ; }); diff --git a/packages/utils/src/url.ts b/packages/utils/src/url.ts index 638839bb06c..2de0e778b89 100644 --- a/packages/utils/src/url.ts +++ b/packages/utils/src/url.ts @@ -257,3 +257,40 @@ export function extractURLComponents(url: URL | string): IURLComponents | undefi return undefined; } } + +/** + * Validates that a next_path parameter is safe for redirection. + * Only allows relative paths starting with "/" to prevent open redirect vulnerabilities. + * + * @param url - The next_path URL to validate + * @returns True if the URL is a safe relative path, false otherwise + * + * @example + * isValidNextPath("/dashboard") // true + * isValidNextPath("/workspace/123") // true + * isValidNextPath("https://malicious.com") // false + * isValidNextPath("javascript:alert(1)") // false + * isValidNextPath("") // false + * isValidNextPath("dashboard") // false (must start with /) + */ +export function isValidNextPath(url: string): boolean { + if (!url || typeof url !== "string") return false; + + // Only allow relative paths starting with / + if (!url.startsWith("/")) return false; + + // Disallow external URLs (http, https, ftp) for security + const disallowedSchemes = /^(https?|ftp):\/\//i; + if (disallowedSchemes.test(url)) return false; + + // Additional security checks for malicious patterns + const maliciousPatterns = [ + /javascript:/i, + /data:/i, + /vbscript:/i, + /