diff --git a/apps/web/pages/auth/login.tsx b/apps/web/pages/auth/login.tsx index 51ee2e606b..94f25ea0fd 100644 --- a/apps/web/pages/auth/login.tsx +++ b/apps/web/pages/auth/login.tsx @@ -7,6 +7,7 @@ import { useRouter } from "next/router"; import { useState } from "react"; import { useForm } from "react-hook-form"; +import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl"; import { Alert } from "@calcom/ui/Alert"; import Button from "@calcom/ui/Button"; import { EmailField, Form, PasswordField } from "@calcom/ui/form/fields"; @@ -61,12 +62,15 @@ export default function Login({ let callbackUrl = typeof router.query?.callbackUrl === "string" ? router.query.callbackUrl : ""; - // If not absolute URL, make it absolute if (/"\//.test(callbackUrl)) callbackUrl = callbackUrl.substring(1); + + // If not absolute URL, make it absolute if (!/^https?:\/\//.test(callbackUrl)) { callbackUrl = `${WEBAPP_URL}/${callbackUrl}`; } + callbackUrl = getSafeRedirectUrl(callbackUrl); + const LoginFooter = ( {t("dont_have_an_account")}{" "} diff --git a/packages/app-store/googlecalendar/api/callback.ts b/packages/app-store/googlecalendar/api/callback.ts index c5e1a69c7f..ff9863f91b 100644 --- a/packages/app-store/googlecalendar/api/callback.ts +++ b/packages/app-store/googlecalendar/api/callback.ts @@ -2,6 +2,7 @@ import { google } from "googleapis"; import type { NextApiRequest, NextApiResponse } from "next"; import { WEBAPP_URL } from "@calcom/lib/constants"; +import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl"; import prisma from "@calcom/prisma"; import { decodeOAuthState } from "../../_utils/decodeOAuthState"; @@ -10,7 +11,6 @@ const credentials = process.env.GOOGLE_API_CREDENTIALS; export default async function handler(req: NextApiRequest, res: NextApiResponse) { const { code } = req.query; - if (code && typeof code !== "string") { res.status(400).json({ message: "`code` must be a string" }); return; @@ -19,7 +19,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) res.status(400).json({ message: "There are no Google Credentials installed." }); return; } - const { client_secret, client_id } = JSON.parse(credentials).web; const redirect_uri = WEBAPP_URL + "/api/integrations/googlecalendar/callback"; @@ -41,5 +40,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }, }); const state = decodeOAuthState(req); - res.redirect(state?.returnTo ?? "/apps/installed"); + res.redirect(getSafeRedirectUrl(state?.returnTo) ?? "/apps/installed"); } diff --git a/packages/app-store/hubspotothercalendar/api/callback.ts b/packages/app-store/hubspotothercalendar/api/callback.ts index a99a7ee7d4..b3483e45b9 100644 --- a/packages/app-store/hubspotothercalendar/api/callback.ts +++ b/packages/app-store/hubspotothercalendar/api/callback.ts @@ -3,6 +3,7 @@ import { TokenResponseIF } from "@hubspot/api-client/lib/codegen/oauth/models/To import type { NextApiRequest, NextApiResponse } from "next"; import { WEBAPP_URL } from "@calcom/lib/constants"; +import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl"; import prisma from "@calcom/prisma"; import { decodeOAuthState } from "../../_utils/decodeOAuthState"; @@ -52,5 +53,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }); const state = decodeOAuthState(req); - res.redirect(state?.returnTo ?? "/apps/installed"); + res.redirect(getSafeRedirectUrl(state?.returnTo) ?? "/apps/installed"); } diff --git a/packages/app-store/office365calendar/api/callback.ts b/packages/app-store/office365calendar/api/callback.ts index 940f74f062..91e04b4c6e 100644 --- a/packages/app-store/office365calendar/api/callback.ts +++ b/packages/app-store/office365calendar/api/callback.ts @@ -1,6 +1,7 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { BASE_URL } from "@calcom/lib/constants"; +import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl"; import prisma from "@calcom/prisma"; import { decodeOAuthState } from "../../_utils/decodeOAuthState"; @@ -62,5 +63,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }); const state = decodeOAuthState(req); - return res.redirect(state?.returnTo ?? "/apps/installed"); + return res.redirect(getSafeRedirectUrl(state?.returnTo) ?? "/apps/installed"); } diff --git a/packages/app-store/office365video/api/callback.ts b/packages/app-store/office365video/api/callback.ts index 2ef3f1a127..94e26cbd0d 100644 --- a/packages/app-store/office365video/api/callback.ts +++ b/packages/app-store/office365video/api/callback.ts @@ -1,6 +1,7 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { BASE_URL } from "@calcom/lib/constants"; +import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl"; import prisma from "@calcom/prisma"; import { decodeOAuthState } from "../../_utils/decodeOAuthState"; @@ -63,5 +64,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }); const state = decodeOAuthState(req); - return res.redirect(state?.returnTo ?? "/apps/installed"); + return res.redirect(getSafeRedirectUrl(state?.returnTo) ?? "/apps/installed"); } diff --git a/packages/lib/getSafeRedirectUrl.ts b/packages/lib/getSafeRedirectUrl.ts new file mode 100644 index 0000000000..c7beb6cbd9 --- /dev/null +++ b/packages/lib/getSafeRedirectUrl.ts @@ -0,0 +1,16 @@ +import { WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants"; + +// It ensures that redirection URL safe where it is accepted through a query params or other means where user can change it. +export const getSafeRedirectUrl = (url: string | undefined) => { + url = url || ""; + if (url.search(/^https?:\/\//) === -1) { + throw new Error("Pass an absolute URL"); + } + + // Avoid open redirection security vulnerability + if (!url.startsWith(WEBAPP_URL) && !url.startsWith(WEBSITE_URL)) { + url = `${WEBAPP_URL}/`; + } + + return url; +};