From 3e2c991344df0c0e1ef8dc6b44b6b555c0dbcc06 Mon Sep 17 00:00:00 2001 From: Emran-Y Date: Tue, 28 Jul 2026 16:03:28 +0200 Subject: [PATCH] fix: clamp page margin values to [0, 100] to prevent crash on paste When a user pastes a large value (e.g. '1224') into the margin input field, the value bypasses the HTML and gets stored directly into resume metadata. The PDF renderer then cannot handle an absurdly large margin, crashing the builder entirely. This fix adds two layers of defense: 1. Zod schema: add .max(100) to marginX and marginY in pageSchema, so any value > 100 is rejected at the validation level. 2. Input handler: clamp the onChange value to [0, 100] so the input never even temporarily stores an out-of-range value. The same pattern can apply to any other numeric inputs that need a reasonable upper bound. Fixes: #3263 --- .../$resumeId/-sidebar/right/sections/page.tsx | 18 ++++++++++++------ packages/schema/src/resume/data.ts | 4 ++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/apps/web/src/routes/builder/$resumeId/-sidebar/right/sections/page.tsx b/apps/web/src/routes/builder/$resumeId/-sidebar/right/sections/page.tsx index d1b91bea5..d5bb2fbc9 100644 --- a/apps/web/src/routes/builder/$resumeId/-sidebar/right/sections/page.tsx +++ b/apps/web/src/routes/builder/$resumeId/-sidebar/right/sections/page.tsx @@ -53,11 +53,16 @@ function PageSectionForm() { persist({ ...form.state.values, [name]: value }); }; - const pageNumberFields = [ - { name: "marginX" as const, label: Margin (Horizontal), min: 0, max: 100 as number | undefined }, - { name: "marginY" as const, label: Margin (Vertical), min: 0, max: 100 as number | undefined }, - { name: "gapX" as const, label: Spacing (Horizontal), min: 0, max: undefined }, - { name: "gapY" as const, label: Spacing (Vertical), min: 0, max: undefined }, + const CLAMP_MIN = 0; +const CLAMP_MAX = 100; + +const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max); + +const pageNumberFields = [ + { name: "marginX" as const, label: Margin (Horizontal), min: CLAMP_MIN, max: CLAMP_MAX }, + { name: "marginY" as const, label: Margin (Vertical), min: CLAMP_MIN, max: CLAMP_MAX }, + { name: "gapX" as const, label: Spacing (Horizontal), min: 0, max: undefined }, + { name: "gapY" as const, label: Spacing (Vertical), min: 0, max: undefined }, ]; const pageSwitchFields = [ @@ -151,7 +156,8 @@ function PageSectionForm() { onBlur={field.handleBlur} onChange={(e) => { const v = e.target.value; - const num = v === "" ? ("" as unknown as number) : Number(v); + const raw = v === "" ? ("" as unknown as number) : Number(v); + const num = max !== undefined && typeof raw === "number" ? clamp(raw, min ?? 0, max) : raw; field.handleChange(num); handleAutoSave(name, num); }} diff --git a/packages/schema/src/resume/data.ts b/packages/schema/src/resume/data.ts index e7603cb87..d1beb52a3 100644 --- a/packages/schema/src/resume/data.ts +++ b/packages/schema/src/resume/data.ts @@ -444,8 +444,8 @@ export const layoutSchema = z.object({ export const pageSchema = z.object({ gapX: z.number().min(0).describe("The horizontal gap between the sections of the page, defined in points (pt)."), gapY: z.number().min(0).describe("The vertical gap between the sections of the page, defined in points (pt)."), - marginX: z.number().min(0).describe("The horizontal margin of the page, defined in points (pt)."), - marginY: z.number().min(0).describe("The vertical margin of the page, defined in points (pt)."), + marginX: z.number().min(0).max(100).describe("The horizontal margin of the page, defined in points (pt)."), + marginY: z.number().min(0).max(100).describe("The vertical margin of the page, defined in points (pt)."), format: z .enum(["a4", "letter", "free-form"]) .describe("The format of the page. Can be 'a4', 'letter', or 'free-form'.")