From 4088f9abd5b31955e27cab90e7cdad854ee34a74 Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 16 Mar 2026 13:35:03 +0000 Subject: [PATCH] feat: sanitize input for checklists --- packages/api/src/routers/checklist.ts | 47 +++++++++++++-------------- packages/shared/src/utils/index.ts | 1 + packages/shared/src/utils/sanitize.ts | 6 ++++ 3 files changed, 30 insertions(+), 24 deletions(-) create mode 100644 packages/shared/src/utils/sanitize.ts diff --git a/packages/api/src/routers/checklist.ts b/packages/api/src/routers/checklist.ts index 2c783c3b..a9575947 100644 --- a/packages/api/src/routers/checklist.ts +++ b/packages/api/src/routers/checklist.ts @@ -4,6 +4,7 @@ import { z } from "zod"; import * as cardRepo from "@kan/db/repository/card.repo"; import * as cardActivityRepo from "@kan/db/repository/cardActivity.repo"; import * as checklistRepo from "@kan/db/repository/checklist.repo"; +import { stripHtml } from "@kan/shared/utils"; import { createTRPCRouter, protectedProcedure } from "../trpc"; import { assertPermission } from "../utils/permissions"; @@ -223,7 +224,7 @@ export const checklistRouter = createTRPCRouter({ .input( z.object({ checklistPublicId: z.string().length(12), - title: z.string().min(1).max(500), + title: z.string().min(1).max(500).transform(stripHtml), }), ) .output(checklistItemSchema) @@ -288,7 +289,7 @@ export const checklistRouter = createTRPCRouter({ .input( z.object({ checklistItemPublicId: z.string().length(12), - title: z.string().min(1).max(500).optional(), + title: z.string().min(1).max(500).transform(stripHtml).optional(), completed: z.boolean().optional(), index: z.number().int().min(0).optional(), }), @@ -322,30 +323,29 @@ export const checklistRouter = createTRPCRouter({ const previousTitle = item.title; - let updatedItem; + let updatedItem; - if (input.title !== undefined || input.completed !== undefined) { - updatedItem = await checklistRepo.updateItemById(ctx.db, { - id: item.id, - title: input.title, - completed: input.completed, - }); - } + if (input.title !== undefined || input.completed !== undefined) { + updatedItem = await checklistRepo.updateItemById(ctx.db, { + id: item.id, + title: input.title, + completed: input.completed, + }); + } - if (input.index !== undefined) { - updatedItem = await checklistRepo.reorderItem(ctx.db, { - itemId: item.id, - newIndex: input.index, - }); - } - - if (!updatedItem) { - throw new TRPCError({ - message: `Failed to update checklist item`, - code: "INTERNAL_SERVER_ERROR", - }); - } + if (input.index !== undefined) { + updatedItem = await checklistRepo.reorderItem(ctx.db, { + itemId: item.id, + newIndex: input.index, + }); + } + if (!updatedItem) { + throw new TRPCError({ + message: `Failed to update checklist item`, + code: "INTERNAL_SERVER_ERROR", + }); + } // Log completion toggle if (input.completed !== undefined) { @@ -371,7 +371,6 @@ export const checklistRouter = createTRPCRouter({ } return updatedItem; - }), deleteItem: protectedProcedure .meta({ diff --git a/packages/shared/src/utils/index.ts b/packages/shared/src/utils/index.ts index 28c63eb6..86997d9b 100644 --- a/packages/shared/src/utils/index.ts +++ b/packages/shared/src/utils/index.ts @@ -5,3 +5,4 @@ export * from "./email"; export * from "./dueDateFilters"; export * from "./s3"; export * from "./mentions"; +export * from "./sanitize"; diff --git a/packages/shared/src/utils/sanitize.ts b/packages/shared/src/utils/sanitize.ts new file mode 100644 index 00000000..fe17b1ff --- /dev/null +++ b/packages/shared/src/utils/sanitize.ts @@ -0,0 +1,6 @@ +/** + * Strips all HTML tags from a string, returning plain text. + * Use this on any user-supplied text field before storing or displaying. + */ +export const stripHtml = (value: string): string => + value.replace(/<[^>]*>/g, "").trim();