From 0f4240d3dbeca2b83992e894506ce94484f003e6 Mon Sep 17 00:00:00 2001 From: Buco7854 Date: Wed, 10 Jun 2026 10:02:39 +0000 Subject: [PATCH] feat: cap custom CSS length and add validation tests Bound customCSS to 100000 characters on both the client schema and the server body validation (the server is the enforcement boundary; multer's 1MB field cap was the only limit), regenerate the OpenAPI spec, and add a validation test suite covering acceptance, clearing, the length cap, and type rejection. --- client/src/Validation/statusPage.ts | 6 ++- server/openapi.json | 6 ++- .../api/validation/statusPageValidation.ts | 2 +- .../validation/statusPageValidation.test.ts | 51 +++++++++++++++++++ 4 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 server/test/unit/validation/statusPageValidation.test.ts diff --git a/client/src/Validation/statusPage.ts b/client/src/Validation/statusPage.ts index 542f6b2ae..3533fe017 100644 --- a/client/src/Validation/statusPage.ts +++ b/client/src/Validation/statusPage.ts @@ -35,7 +35,11 @@ export const statusPageSchema = z.object({ showUptimePercentage: z.boolean().register(statusPageStepRegistry, { step: 1 }), showAdminLoginLink: z.boolean().register(statusPageStepRegistry, { step: 1 }), showInfrastructure: z.boolean().register(statusPageStepRegistry, { step: 1 }), - customCSS: z.string().optional().register(statusPageStepRegistry, { step: 1 }), + customCSS: z + .string() + .max(100000, "Custom CSS must be at most 100000 characters") + .optional() + .register(statusPageStepRegistry, { step: 1 }), theme: z .enum(STATUS_PAGE_THEMES) .optional() diff --git a/server/openapi.json b/server/openapi.json index 838c94863..ae34fba04 100644 --- a/server/openapi.json +++ b/server/openapi.json @@ -10488,7 +10488,8 @@ "type": "boolean" }, "customCSS": { - "type": "string" + "type": "string", + "maxLength": 100000 }, "removeLogo": { "anyOf": [ @@ -10694,7 +10695,8 @@ "type": "boolean" }, "customCSS": { - "type": "string" + "type": "string", + "maxLength": 100000 }, "removeLogo": { "anyOf": [ diff --git a/server/src/api/validation/statusPageValidation.ts b/server/src/api/validation/statusPageValidation.ts index 3634d831f..c27ca3f15 100644 --- a/server/src/api/validation/statusPageValidation.ts +++ b/server/src/api/validation/statusPageValidation.ts @@ -32,7 +32,7 @@ export const createStatusPageBodyValidation = z showUptimePercentage: booleanCoercion, showAdminLoginLink: booleanCoercion.optional(), showInfrastructure: booleanCoercion.optional(), - customCSS: z.string().optional(), + customCSS: z.string().max(100000, "Custom CSS must be at most 100000 characters").optional(), removeLogo: z.union([z.literal("true"), z.literal("false")]).optional(), theme: z.enum(StatusPageThemes).optional(), themeMode: z.enum(StatusPageThemeModes).optional(), diff --git a/server/test/unit/validation/statusPageValidation.test.ts b/server/test/unit/validation/statusPageValidation.test.ts new file mode 100644 index 000000000..bc67efc42 --- /dev/null +++ b/server/test/unit/validation/statusPageValidation.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from "@jest/globals"; +import { createStatusPageBodyValidation } from "../../../src/api/validation/statusPageValidation.ts"; + +const baseCreateBody = (overrides: Record = {}) => ({ + type: "uptime", + companyName: "Test Co", + url: "my-status-page", + monitors: ["0123456789abcdef01234567"], + isPublished: true, + showUptimePercentage: true, + ...overrides, +}); + +describe("createStatusPageBodyValidation", () => { + it("accepts a valid body without customCSS", () => { + const result = createStatusPageBodyValidation.safeParse(baseCreateBody()); + expect(result.success).toBe(true); + }); + + it("accepts and preserves customCSS", () => { + const customCSS = ".status-page > footer { display: none; }"; + const result = createStatusPageBodyValidation.safeParse(baseCreateBody({ customCSS })); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.customCSS).toBe(customCSS); + } + }); + + it("accepts an empty customCSS so it can be cleared", () => { + const result = createStatusPageBodyValidation.safeParse(baseCreateBody({ customCSS: "" })); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.customCSS).toBe(""); + } + }); + + it("rejects customCSS over the maximum length", () => { + const result = createStatusPageBodyValidation.safeParse(baseCreateBody({ customCSS: "a".repeat(100001) })); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues).toEqual( + expect.arrayContaining([expect.objectContaining({ message: "Custom CSS must be at most 100000 characters", path: ["customCSS"] })]) + ); + } + }); + + it("rejects a non-string customCSS", () => { + const result = createStatusPageBodyValidation.safeParse(baseCreateBody({ customCSS: 42 })); + expect(result.success).toBe(false); + }); +});