diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..8962e8ee --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,14 @@ +# Security Policy + +If you discover a security vulnerability, please follow the steps below to responsibly disclose it to us: + +1. **Do not create a public GitHub issue or discussion post.** This could put the security of other users at risk. +2. Send a detailed report to [security@fossorial.io](mailto:security@fossorial.io) or send a **private** message to a maintainer on [Discord](https://discord.gg/HCJR8Xhme4). Include: + +- Description and location of the vulnerability. +- Potential impact of the vulnerability. +- Steps to reproduce the vulnerability. +- Potential solutions to fix the vulnerability. +- Your name/handle and a link for recognition (optional). + +We aim to address the issue as soon as possible. diff --git a/install/Makefile b/install/Makefile index 647bff8b..cca07018 100644 --- a/install/Makefile +++ b/install/Makefile @@ -2,7 +2,11 @@ all: build build: - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o installer + CGO_ENABLED=0 GOOS=linux go build -o installer + +all_arches: + CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o installer_linux_arm64 + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o installer_linux_amd64 clean: rm installer \ No newline at end of file diff --git a/package.json b/package.json index e05785bf..ed104bb0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,10 @@ { "name": "@fosrl/pangolin", +<<<<<<< HEAD "version": "1.0.0-beta.2", +======= + "version": "1.0.0-beta.3", +>>>>>>> c3d19454f7f5c5546a7efb47c23fc040dbbf94d2 "private": true, "type": "module", "description": "Tunneled Reverse Proxy Management Server with Identity and Access Control and Dashboard UI", diff --git a/server/lib/config.ts b/server/lib/config.ts index 35540ba7..468f0e2c 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -3,7 +3,15 @@ import yaml from "js-yaml"; import path from "path"; import { z } from "zod"; import { fromError } from "zod-validation-error"; +<<<<<<< HEAD import { __DIRNAME, APP_PATH, configFilePath1, configFilePath2 } from "@server/lib/consts"; +======= +import { + __DIRNAME, + configFilePath1, + configFilePath2 +} from "@server/lib/consts"; +>>>>>>> c3d19454f7f5c5546a7efb47c23fc040dbbf94d2 import { loadAppVersion } from "@server/lib/loadAppVersion"; import { passwordSchema } from "@server/auth/passwordSchema"; @@ -11,9 +19,15 @@ const portSchema = z.number().positive().gt(0).lte(65535); const hostnameSchema = z .string() .regex( +<<<<<<< HEAD /^(?!-)[a-zA-Z0-9-]{1,63}(?>>>>>> c3d19454f7f5c5546a7efb47c23fc040dbbf94d2 const environmentSchema = z.object({ app: z.object({ diff --git a/src/app/[orgId]/layout.tsx b/src/app/[orgId]/layout.tsx index b8b5c6a9..fa41beb2 100644 --- a/src/app/[orgId]/layout.tsx +++ b/src/app/[orgId]/layout.tsx @@ -25,7 +25,7 @@ export default async function OrgLayout(props: { const user = await getUser(); if (!user) { - redirect(`/?redirect=/${orgId}`); + redirect(`/`); } try { diff --git a/src/app/[orgId]/settings/general/layout.tsx b/src/app/[orgId]/settings/general/layout.tsx index 5923118e..4b41b8c3 100644 --- a/src/app/[orgId]/settings/general/layout.tsx +++ b/src/app/[orgId]/settings/general/layout.tsx @@ -26,7 +26,7 @@ export default async function GeneralSettingsPage({ const user = await getUser(); if (!user) { - redirect(`/?redirect=/${orgId}/settings/general`); + redirect(`/`); } let orgUser = null; diff --git a/src/app/[orgId]/settings/layout.tsx b/src/app/[orgId]/settings/layout.tsx index 95a6cc00..b0b561a2 100644 --- a/src/app/[orgId]/settings/layout.tsx +++ b/src/app/[orgId]/settings/layout.tsx @@ -61,7 +61,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { const user = await getUser(); if (!user) { - redirect(`/?redirect=/${params.orgId}/`); + redirect(`/`); } const cookie = await authCookieHeader(); diff --git a/src/app/auth/login/DashboardLoginForm.tsx b/src/app/auth/login/DashboardLoginForm.tsx index 088fc631..715a0fb9 100644 --- a/src/app/auth/login/DashboardLoginForm.tsx +++ b/src/app/auth/login/DashboardLoginForm.tsx @@ -13,6 +13,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext"; import { useRouter } from "next/navigation"; import { useEffect } from "react"; import Image from "next/image"; +import { cleanRedirect } from "@app/lib/cleanRedirect"; type DashboardLoginFormProps = { redirect?: string; @@ -57,10 +58,9 @@ export default function DashboardLoginForm({ { - if (redirect && redirect.includes("http")) { - window.location.href = redirect; - } else if (redirect) { - router.push(redirect); + if (redirect) { + const safe = cleanRedirect(redirect); + router.push(safe); } else { router.push("/"); } diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx index 87c27071..118cfcd0 100644 --- a/src/app/auth/login/page.tsx +++ b/src/app/auth/login/page.tsx @@ -5,6 +5,7 @@ import { cache } from "react"; import DashboardLoginForm from "./DashboardLoginForm"; import { Mail } from "lucide-react"; import { pullEnv } from "@app/lib/pullEnv"; +import { cleanRedirect } from "@app/lib/cleanRedirect"; export const dynamic = "force-dynamic"; @@ -25,6 +26,11 @@ export default async function Page(props: { redirect("/"); } + let redirectUrl: string | undefined = undefined; + if (searchParams.redirect) { + redirectUrl = cleanRedirect(searchParams.redirect as string); + } + return ( <> {isInvite && ( @@ -42,16 +48,16 @@ export default async function Page(props: { )} - + {(!signUpDisabled || isInvite) && (

Don't have an account?{" "} diff --git a/src/app/auth/reset-password/ResetPasswordForm.tsx b/src/app/auth/reset-password/ResetPasswordForm.tsx index a9232d4d..ae997818 100644 --- a/src/app/auth/reset-password/ResetPasswordForm.tsx +++ b/src/app/auth/reset-password/ResetPasswordForm.tsx @@ -43,6 +43,7 @@ import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp"; import { passwordSchema } from "@server/auth/passwordSchema"; +import { cleanRedirect } from "@app/lib/cleanRedirect"; const requestSchema = z.object({ email: z.string().email() @@ -186,11 +187,9 @@ export default function ResetPasswordForm({ setSuccessMessage("Password reset successfully! Back to login..."); setTimeout(() => { - if (redirect && redirect.includes("http")) { - window.location.href = redirect; - } if (redirect) { - router.push(redirect); + const safe = cleanRedirect(redirect); + router.push(safe); } else { router.push("/login"); } diff --git a/src/app/auth/reset-password/page.tsx b/src/app/auth/reset-password/page.tsx index b5636c42..73654beb 100644 --- a/src/app/auth/reset-password/page.tsx +++ b/src/app/auth/reset-password/page.tsx @@ -3,6 +3,7 @@ import { redirect } from "next/navigation"; import { cache } from "react"; import ResetPasswordForm from "./ResetPasswordForm"; import Link from "next/link"; +import { cleanRedirect } from "@app/lib/cleanRedirect"; export const dynamic = "force-dynamic"; @@ -21,6 +22,11 @@ export default async function Page(props: { redirect("/"); } + let redirectUrl: string | undefined = undefined; + if (searchParams.redirect) { + redirectUrl = cleanRedirect(searchParams.redirect); + } + return ( <> diff --git a/src/app/auth/resource/[resourceId]/ResourceAuthPortal.tsx b/src/app/auth/resource/[resourceId]/ResourceAuthPortal.tsx index a25edf74..c23403a8 100644 --- a/src/app/auth/resource/[resourceId]/ResourceAuthPortal.tsx +++ b/src/app/auth/resource/[resourceId]/ResourceAuthPortal.tsx @@ -481,11 +481,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { className={`${numMethods <= 1 ? "mt-0" : ""}`} > await handleSSOAuth() } diff --git a/src/app/auth/resource/[resourceId]/page.tsx b/src/app/auth/resource/[resourceId]/page.tsx index 886801c7..2cf37848 100644 --- a/src/app/auth/resource/[resourceId]/page.tsx +++ b/src/app/auth/resource/[resourceId]/page.tsx @@ -55,7 +55,17 @@ export default async function ResourceAuthPage(props: { ); } - const redirectUrl = searchParams.redirect || authInfo.url; + let redirectUrl = authInfo.url; + if (searchParams.redirect) { + try { + const serverResourceHost = new URL(authInfo.url).host; + const redirectHost = new URL(searchParams.redirect).host; + + if (serverResourceHost === redirectHost) { + redirectUrl = searchParams.redirect; + } + } catch (e) {} + } const hasAuth = authInfo.password || diff --git a/src/app/auth/signup/SignupForm.tsx b/src/app/auth/signup/SignupForm.tsx index 9630d907..f839284e 100644 --- a/src/app/auth/signup/SignupForm.tsx +++ b/src/app/auth/signup/SignupForm.tsx @@ -30,6 +30,7 @@ import { formatAxiosError } from "@app/lib/api"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import Image from "next/image"; +import { cleanRedirect } from "@app/lib/cleanRedirect"; type SignupFormProps = { redirect?: string; @@ -92,17 +93,17 @@ export default function SignupForm({ if (res.data?.data?.emailVerificationRequired) { if (redirect) { - router.push(`/auth/verify-email?redirect=${redirect}`); + const safe = cleanRedirect(redirect); + router.push(`/auth/verify-email?redirect=${safe}`); } else { router.push("/auth/verify-email"); } return; } - if (redirect && redirect.includes("http")) { - window.location.href = redirect; - } else if (redirect) { - router.push(redirect); + if (redirect) { + const safe = cleanRedirect(redirect); + router.push(safe); } else { router.push("/"); } diff --git a/src/app/auth/signup/page.tsx b/src/app/auth/signup/page.tsx index f53ff2c8..361cc0db 100644 --- a/src/app/auth/signup/page.tsx +++ b/src/app/auth/signup/page.tsx @@ -1,5 +1,6 @@ import SignupForm from "@app/app/auth/signup/SignupForm"; import { verifySession } from "@app/lib/auth/verifySession"; +import { cleanRedirect } from "@app/lib/cleanRedirect"; import { pullEnv } from "@app/lib/pullEnv"; import { Mail } from "lucide-react"; import Link from "next/link"; @@ -41,6 +42,11 @@ export default async function Page(props: { } } + let redirectUrl: string | undefined; + if (searchParams.redirect) { + redirectUrl = cleanRedirect(searchParams.redirect); + } + return ( <> {isInvite && ( @@ -59,7 +65,7 @@ export default async function Page(props: { )} @@ -68,9 +74,9 @@ export default async function Page(props: { Already have an account?{" "} diff --git a/src/app/auth/verify-email/VerifyEmailForm.tsx b/src/app/auth/verify-email/VerifyEmailForm.tsx index 7a6bc082..8a0ca89a 100644 --- a/src/app/auth/verify-email/VerifyEmailForm.tsx +++ b/src/app/auth/verify-email/VerifyEmailForm.tsx @@ -36,6 +36,7 @@ import { useRouter } from "next/navigation"; import { formatAxiosError } from "@app/lib/api";; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; +import { cleanRedirect } from "@app/lib/cleanRedirect"; const FormSchema = z.object({ email: z.string().email({ message: "Invalid email address" }), @@ -91,11 +92,9 @@ export default function VerifyEmailForm({ "Email successfully verified! Redirecting you..." ); setTimeout(() => { - if (redirect && redirect.includes("http")) { - window.location.href = redirect; - } if (redirect) { - router.push(redirect); + const safe = cleanRedirect(redirect); + router.push(safe); } else { router.push("/"); } diff --git a/src/app/auth/verify-email/page.tsx b/src/app/auth/verify-email/page.tsx index 3452df69..033fa75d 100644 --- a/src/app/auth/verify-email/page.tsx +++ b/src/app/auth/verify-email/page.tsx @@ -1,5 +1,6 @@ import VerifyEmailForm from "@app/app/auth/verify-email/VerifyEmailForm"; import { verifySession } from "@app/lib/auth/verifySession"; +import { cleanRedirect } from "@app/lib/cleanRedirect"; import { pullEnv } from "@app/lib/pullEnv"; import { redirect } from "next/navigation"; import { cache } from "react"; @@ -27,11 +28,16 @@ export default async function Page(props: { redirect("/"); } + let redirectUrl: string | undefined; + if (searchParams.redirect) { + redirectUrl = cleanRedirect(searchParams.redirect as string); + } + return ( <> ); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 16ce9963..b4abbad3 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -6,6 +6,8 @@ import { ThemeProvider } from "@app/providers/ThemeProvider"; import EnvProvider from "@app/providers/EnvProvider"; import { Separator } from "@app/components/ui/separator"; import { pullEnv } from "@app/lib/pullEnv"; +import { BookOpenText } from "lucide-react"; +import Image from "next/image"; export const metadata: Metadata = { title: `Dashboard - Pangolin`, @@ -38,10 +40,10 @@ export default async function RootLayout({

{children}
{/* Footer */} -