From d418c367cb956841adcc323de80c21615249c910 Mon Sep 17 00:00:00 2001 From: Catherine Renelle <32781029+catrenelle@users.noreply.github.com> Date: Sat, 7 Mar 2026 18:36:47 -0500 Subject: [PATCH] Security hardening and fix all compile warnings Security fixes: - Validate theme background image URLs against whitelist pattern - Add board permission check to Socket.IO join-board handler - Disable raw HTML passthrough in markdown parser (XSS prevention) - Add UUID validation on avatar and branding route params (path traversal) - Add security headers (X-Frame-Options, X-Content-Type-Options, Referrer-Policy) - Validate webhook URLs (HTTPS only, block internal addresses) - Add import payload size limit (5MB) - Remove server uptime from health endpoint - Make seed password configurable via SEED_ADMIN_PASSWORD env var - Patch DOMPurify dependency vulnerability via npm audit fix Warning fixes: - Convert $state(prop) to $effect.pre sync pattern (state_referenced_locally) - Add for/id pairings to form labels (a11y_label_has_associated_control) - Add svelte-ignore for intentional autofocus usage (a11y_autofocus) - Add ARIA roles to interactive/overlay divs (a11y_no_static_element_interactions) - Add aria-label to icon-only buttons (a11y_consider_explicit_label) - Add keyboard handler alongside click events (a11y_click_events_have_key_events) - Fix non-reactive fileInput declaration Co-Authored-By: Claude Opus 4.6 --- package-lock.json | 15 +++++++----- prisma/seed.ts | 5 ++-- server.js | 24 ++++++++++++++++++- src/hooks.server.ts | 5 ++++ src/lib/components/AssigneePicker.svelte | 4 ++-- src/lib/components/AttachmentList.svelte | 4 ++-- src/lib/components/AutomationBuilder.svelte | 10 ++++---- .../components/BoardBackgroundPicker.svelte | 10 +++++++- src/lib/components/BoardSettingsModal.svelte | 16 +++++++++---- src/lib/components/CardModal.svelte | 8 ++++++- src/lib/components/ChecklistSection.svelte | 1 + src/lib/components/ColorPicker.svelte | 3 ++- src/lib/components/ColumnSortMenu.svelte | 1 + src/lib/components/CommentList.svelte | 10 ++++---- src/lib/components/CustomFieldEditor.svelte | 9 ++++++- src/lib/components/DependencySection.svelte | 1 + src/lib/components/ImageUploadField.svelte | 3 ++- src/lib/components/KeyboardShortcuts.svelte | 1 + src/lib/components/MarkdownEditor.svelte | 13 +++++----- src/lib/components/TagPicker.svelte | 2 ++ src/lib/server/theme-css.ts | 13 ++++++---- src/lib/utils/markdown.ts | 15 +++++++++--- src/lib/utils/theme-persist.ts | 13 ++++++---- src/lib/utils/theme-preview.ts | 9 +++++-- src/routes/account/+page.svelte | 15 ++++++++---- src/routes/admin/+page.svelte | 13 ++++++---- src/routes/admin/branding/+page.svelte | 21 +++++++++------- src/routes/admin/site/+page.svelte | 8 +++++-- src/routes/api/admin/webhooks/+server.ts | 14 +++++++++++ src/routes/api/avatars/[userId]/+server.ts | 3 +++ src/routes/api/boards/import/+server.ts | 5 ++++ .../api/branding/[tenantId]/[slot]/+server.ts | 2 ++ src/routes/api/health/+server.ts | 2 +- src/routes/boards/+page.svelte | 2 +- src/routes/boards/[boardId]/+page.svelte | 12 ++++++---- .../boards/[boardId]/gantt/+page.svelte | 8 +++++-- .../boards/[boardId]/members/+page.svelte | 5 +++- 37 files changed, 227 insertions(+), 78 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5e59585..87f35e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1051,9 +1051,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.53.2", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.53.2.tgz", - "integrity": "sha512-M+MqAvFve12T1HWws/2npP/s3hFtyjw3GB/OXW/8a1jZBk48qnvPJrtgE+VOMc3RnjUMxc4mv/vQ73nvj2uNMg==", + "version": "2.53.4", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.53.4.tgz", + "integrity": "sha512-iAIPEahFgDJJyvz8g0jP08KvqnM6JvdW8YfsygZ+pMeMvyM2zssWMltcsotETvjSZ82G3VlitgDtBIvpQSZrTA==", "license": "MIT", "peer": true, "dependencies": { @@ -2025,10 +2025,13 @@ "license": "MIT" }, "node_modules/dompurify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", - "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz", + "integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==", "license": "(MPL-2.0 OR Apache-2.0)", + "engines": { + "node": ">=20" + }, "optionalDependencies": { "@types/trusted-types": "^2.0.7" } diff --git a/prisma/seed.ts b/prisma/seed.ts index c6a0bcb..d6f72a1 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -22,7 +22,8 @@ async function main() { } // Create admin user - const passwordHash = await bcrypt.hash('admin123', 12); + const seedPassword = process.env.SEED_ADMIN_PASSWORD || 'admin123'; + const passwordHash = await bcrypt.hash(seedPassword, 12); await prisma.user.upsert({ where: { tenantId_email: { tenantId: tenant.id, email: 'admin@example.com' } }, update: {}, @@ -90,7 +91,7 @@ async function main() { } console.log('Seed complete.'); - console.log(' Admin: admin@example.com / admin123'); + console.log(' Admin: admin@example.com (password set via SEED_ADMIN_PASSWORD env var)'); } main() diff --git a/server.js b/server.js index 4717bb4..482cd07 100644 --- a/server.js +++ b/server.js @@ -87,7 +87,29 @@ io.on('connection', (socket) => { // Track which boards this socket has joined (for cleanup on disconnect) const joinedBoards = new Set(); - socket.on('join-board', (boardId) => { + socket.on('join-board', async (boardId) => { + // Validate boardId format (UUID) + if (typeof boardId !== 'string' || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(boardId)) { + return; + } + + // Check board access permission + try { + const board = await prisma.board.findUnique({ + where: { id: boardId }, + select: { visibility: true, members: { select: { userId: true } } } + }); + if (!board) return; + if (board.visibility !== 'PUBLIC') { + if (!user) return; + const isMember = board.members.some((m) => m.userId === user.id); + if (!isMember && user.tenantRole !== 'ADMIN' && user.globalRole !== 'SITE_ADMIN') return; + } + } catch (err) { + logger.error({ err, boardId }, 'Board access check failed'); + return; + } + socket.join(`board:${boardId}`); joinedBoards.add(boardId); diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 6bb081e..76a5bfe 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -67,6 +67,11 @@ export const handle: Handle = async ({ event, resolve }) => { : undefined }); + // Security headers + response.headers.set('X-Frame-Options', 'DENY'); + response.headers.set('X-Content-Type-Options', 'nosniff'); + response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin'); + logger.info({ method: event.request.method, path: event.url.pathname, diff --git a/src/lib/components/AssigneePicker.svelte b/src/lib/components/AssigneePicker.svelte index 3a81ca9..5281b5e 100644 --- a/src/lib/components/AssigneePicker.svelte +++ b/src/lib/components/AssigneePicker.svelte @@ -64,7 +64,7 @@ {assignee.user.name} {#if canEdit} - {#if showDropdown} -
(showDropdown = false)}>
+
{#each boardMembers as member} diff --git a/src/lib/components/AttachmentList.svelte b/src/lib/components/AttachmentList.svelte index 1f6e3c8..e3e8ff6 100644 --- a/src/lib/components/AttachmentList.svelte +++ b/src/lib/components/AttachmentList.svelte @@ -19,11 +19,11 @@ let { cardId, boardId, columnId, attachments = [], canEdit }: Props = $props(); - let localAttachments = $state(attachments); + let localAttachments = $state([]); let uploading = $state(false); let uploadProgress = $state(0); - $effect(() => { + $effect.pre(() => { localAttachments = attachments; }); diff --git a/src/lib/components/AutomationBuilder.svelte b/src/lib/components/AutomationBuilder.svelte index 8d7522b..f7536a8 100644 --- a/src/lib/components/AutomationBuilder.svelte +++ b/src/lib/components/AutomationBuilder.svelte @@ -147,16 +147,16 @@
- - {#each triggers as t} {/each}
- - {#each actions as a} {/each} @@ -244,7 +244,7 @@ class="rounded border-gray-300 dark:border-gray-600 text-[var(--color-primary)] focus:ring-[var(--color-primary)]" /> - {#if open} +