From ae497092d7cf746fbd5b49f471310f3a4693f268 Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Thu, 30 Apr 2026 07:43:20 +0300 Subject: [PATCH] fix: security improvements (#2593) --- apps/remix/app/components/forms/profile.tsx | 6 +- apps/remix/app/components/forms/signup.tsx | 8 +- apps/remix/app/entry.server.tsx | 11 +- apps/remix/app/root.tsx | 29 ++- apps/remix/app/routes/embed+/_v0+/_layout.tsx | 21 +-- apps/remix/app/utils/nonce.ts | 22 +++ apps/remix/server/load-context.ts | 33 ++++ apps/remix/server/main.js | 3 +- apps/remix/server/router.ts | 16 ++ apps/remix/server/security-headers.ts | 169 ++++++++++++++++++ apps/remix/vite.config.ts | 4 + packages/auth/server/types/email-password.ts | 3 +- packages/lib/constants/auth.ts | 15 ++ .../lib/server-only/user/forgot-password.ts | 4 +- .../lib/server-only/user/update-password.ts | 6 + packages/trpc/server/profile-router/schema.ts | 4 +- packages/trpc/server/team-router/schema.ts | 10 +- 17 files changed, 324 insertions(+), 40 deletions(-) create mode 100644 apps/remix/app/utils/nonce.ts create mode 100644 apps/remix/server/load-context.ts create mode 100644 apps/remix/server/security-headers.ts diff --git a/apps/remix/app/components/forms/profile.tsx b/apps/remix/app/components/forms/profile.tsx index dec535fe0..4c5f7a06c 100644 --- a/apps/remix/app/components/forms/profile.tsx +++ b/apps/remix/app/components/forms/profile.tsx @@ -6,6 +6,7 @@ import { useForm } from 'react-hook-form'; import { z } from 'zod'; import { useSession } from '@documenso/lib/client-only/providers/session'; +import { ZNameSchema } from '@documenso/lib/constants/auth'; import { trpc } from '@documenso/trpc/react'; import { cn } from '@documenso/ui/lib/utils'; import { Button } from '@documenso/ui/primitives/button'; @@ -23,10 +24,7 @@ import { SignaturePadDialog } from '@documenso/ui/primitives/signature-pad/signa import { useToast } from '@documenso/ui/primitives/use-toast'; export const ZProfileFormSchema = z.object({ - name: z - .string() - .trim() - .min(1, { message: msg`Please enter a valid name.`.id }), + name: ZNameSchema, signature: z.string().min(1, { message: msg`Signature Pad cannot be empty.`.id }), }); diff --git a/apps/remix/app/components/forms/signup.tsx b/apps/remix/app/components/forms/signup.tsx index 7b767c3db..7fb718790 100644 --- a/apps/remix/app/components/forms/signup.tsx +++ b/apps/remix/app/components/forms/signup.tsx @@ -16,6 +16,7 @@ import { z } from 'zod'; import communityCardsImage from '@documenso/assets/images/community-cards.png'; import { authClient } from '@documenso/auth/client'; import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics'; +import { ZNameSchema } from '@documenso/lib/constants/auth'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { env } from '@documenso/lib/utils/env'; import { zEmail } from '@documenso/lib/utils/zod'; @@ -39,10 +40,7 @@ import { UserProfileTimur } from '~/components/general/user-profile-timur'; export const ZSignUpFormSchema = z .object({ - name: z - .string() - .trim() - .min(1, { message: msg`Please enter a valid name.`.id }), + name: ZNameSchema, email: zEmail().min(1), password: ZPasswordSchema, signature: z.string().min(1, { message: msg`We need your signature to sign documents`.id }), @@ -60,7 +58,7 @@ export const ZSignUpFormSchema = z export const SIGNUP_ERROR_MESSAGES: Record = { SIGNUP_DISABLED: msg`Signup is currently disabled or not available for your email domain.`, - [AppErrorCode.ALREADY_EXISTS]: msg`User with this email already exists. Please use a different email address.`, + [AppErrorCode.ALREADY_EXISTS]: msg`We were unable to create your account. If you already have an account, try signing in instead.`, [AppErrorCode.INVALID_REQUEST]: msg`We were unable to create your account. Please review the information you provided and try again.`, }; diff --git a/apps/remix/app/entry.server.tsx b/apps/remix/app/entry.server.tsx index 6caeba746..d38ef3dea 100644 --- a/apps/remix/app/entry.server.tsx +++ b/apps/remix/app/entry.server.tsx @@ -20,7 +20,7 @@ export default async function handleRequest( responseStatusCode: number, responseHeaders: Headers, routerContext: EntryContext, - _loadContext: AppLoadContext, + loadContext: AppLoadContext, ) { let language = await langCookie.parse(request.headers.get('cookie') ?? ''); @@ -30,6 +30,12 @@ export default async function handleRequest( await dynamicActivate(language); + // Threaded into ServerRouter so React Router applies the nonce to the + // scripts it injects (route manifest, hydration data, module preloads). + // The same nonce is also exposed to the React tree via the root loader so + // our own inline scripts/styles can carry it. + const nonce = loadContext.nonce || undefined; + return new Promise((resolve, reject) => { let shellRendered = false; const userAgent = request.headers.get('user-agent'); @@ -41,9 +47,10 @@ export default async function handleRequest( const { pipe, abort } = renderToPipeableStream( - + , { + nonce, [readyOption]() { shellRendered = true; const body = new PassThrough(); diff --git a/apps/remix/app/root.tsx b/apps/remix/app/root.tsx index 47ebf0262..9fbcecb86 100644 --- a/apps/remix/app/root.tsx +++ b/apps/remix/app/root.tsx @@ -27,6 +27,7 @@ import { GenericErrorLayout } from './components/general/generic-error-layout'; import { langCookie } from './storage/lang-cookie.server'; import { themeSessionResolver } from './storage/theme-session.server'; import { appMetaTags } from './utils/meta'; +import { nonce } from './utils/nonce'; export const links: Route.LinksFunction = () => [{ rel: 'stylesheet', href: stylesheet }]; @@ -41,7 +42,7 @@ export function meta() { */ export const shouldRevalidate = () => false; -export async function loader({ request }: Route.LoaderArgs) { +export async function loader({ context, request }: Route.LoaderArgs) { const session = await getOptionalSession(request); const { getTheme } = await themeSessionResolver(request); @@ -67,6 +68,10 @@ export async function loader({ request }: Route.LoaderArgs) { lang, theme: getTheme(), disableAnimations, + // Surface the per-request CSP nonce produced by `securityHeadersMiddleware` so all + // SSR-rendered + {/* Global license banner currently disabled. Need to wait until after a few releases. */} @@ -152,13 +164,14 @@ export function LayoutContent({ children }: { children: React.ReactNode }) {