From 0076cbaf54434c3eebb74f601a55988ef6b3360b Mon Sep 17 00:00:00 2001 From: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com> Date: Mon, 2 Sep 2024 18:45:57 +0530 Subject: [PATCH] fix: Validation for callback URL (#3061) --- .../(auth)/auth/login/components/SigninForm.tsx | 5 ++--- apps/web/middleware.ts | 4 ++++ packages/lib/utils/{testUrlMatch.ts => url.ts} | 16 ++++++++++++++++ .../components/PageUrlSelector.tsx | 2 +- 4 files changed, 23 insertions(+), 4 deletions(-) rename packages/lib/utils/{testUrlMatch.ts => url.ts} (60%) diff --git a/apps/web/app/(auth)/auth/login/components/SigninForm.tsx b/apps/web/app/(auth)/auth/login/components/SigninForm.tsx index e0c42f3952..2b33b5baf9 100644 --- a/apps/web/app/(auth)/auth/login/components/SigninForm.tsx +++ b/apps/web/app/(auth)/auth/login/components/SigninForm.tsx @@ -50,13 +50,13 @@ export const SigninForm = ({ const searchParams = useSearchParams(); const emailRef = useRef(null); const formMethods = useForm(); - + const callbackUrl = searchParams?.get("callbackUrl"); const onSubmit: SubmitHandler = async (data) => { setLoggingIn(true); try { const signInResponse = await signIn("credentials", { - callbackUrl: searchParams?.get("callbackUrl") || "/", + callbackUrl: callbackUrl ?? "/", email: data.email.toLowerCase(), password: data.password, ...(totpLogin && { totpCode: data.totpCode }), @@ -103,7 +103,6 @@ export const SigninForm = ({ const [signInError, setSignInError] = useState(""); const formRef = useRef(null); const error = searchParams?.get("error"); - const callbackUrl = searchParams?.get("callbackUrl"); const inviteToken = callbackUrl ? new URL(callbackUrl).searchParams.get("token") : null; useEffect(() => { diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index 48ab1c2056..c3ed0f4893 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -17,6 +17,7 @@ import { getToken } from "next-auth/jwt"; import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; import { RATE_LIMITING_DISABLED, WEBAPP_URL } from "@formbricks/lib/constants"; +import { isValidCallbackUrl } from "@formbricks/lib/utils/url"; export const middleware = async (request: NextRequest) => { // issue with next auth types & Next 15; let's review when new fixes are available @@ -28,6 +29,9 @@ export const middleware = async (request: NextRequest) => { } const callbackUrl = request.nextUrl.searchParams.get("callbackUrl"); + if (callbackUrl && !isValidCallbackUrl(callbackUrl, WEBAPP_URL)) { + return NextResponse.json({ error: "Invalid callback URL" }); + } if (token && callbackUrl) { return NextResponse.redirect(WEBAPP_URL + callbackUrl); } diff --git a/packages/lib/utils/testUrlMatch.ts b/packages/lib/utils/url.ts similarity index 60% rename from packages/lib/utils/testUrlMatch.ts rename to packages/lib/utils/url.ts index ccf9f58bdd..3730497d95 100644 --- a/packages/lib/utils/testUrlMatch.ts +++ b/packages/lib/utils/url.ts @@ -22,3 +22,19 @@ export const testURLmatch = ( throw new Error("Invalid match type"); } }; + +// Helper function to validate callback URLs +export const isValidCallbackUrl = (url: string, WEBAPP_URL: string): boolean => { + try { + const parsedUrl = new URL(url); + const allowedSchemes = ["https:", "http:"]; + + // Extract the domain from WEBAPP_URL + const parsedWebAppUrl = new URL(WEBAPP_URL); + const allowedDomains = [parsedWebAppUrl.hostname]; + + return allowedSchemes.includes(parsedUrl.protocol) && allowedDomains.includes(parsedUrl.hostname); + } catch (err) { + return false; + } +}; diff --git a/packages/ui/organisms/NoCodeActionForm/components/PageUrlSelector.tsx b/packages/ui/organisms/NoCodeActionForm/components/PageUrlSelector.tsx index 5554af7d12..fc0d50827c 100644 --- a/packages/ui/organisms/NoCodeActionForm/components/PageUrlSelector.tsx +++ b/packages/ui/organisms/NoCodeActionForm/components/PageUrlSelector.tsx @@ -4,7 +4,7 @@ import { Control, FieldArrayWithId, UseFieldArrayRemove, useFieldArray } from "r import { UseFormReturn } from "react-hook-form"; import toast from "react-hot-toast"; import { cn } from "@formbricks/lib/cn"; -import { testURLmatch } from "@formbricks/lib/utils/testUrlMatch"; +import { testURLmatch } from "@formbricks/lib/utils/url"; import { TActionClassInput, TActionClassPageUrlRule } from "@formbricks/types/action-classes"; import { Alert, AlertDescription, AlertTitle } from "../../../Alert"; import { Button } from "../../../Button";