From 75d12b2950115401371206c9b201ddfc648501b4 Mon Sep 17 00:00:00 2001 From: RaineAllDay Date: Wed, 18 Mar 2026 04:06:34 -0600 Subject: [PATCH] security updates --- scripts/redeploy.sh | 2 +- src/hooks.server.js | 39 ++++++++++++++++++++++++++++++ src/routes/admin/+page.server.js | 11 ++++++--- src/routes/admin/+page.svelte | 5 +--- src/routes/library/+page.server.js | 11 ++++++--- 5 files changed, 56 insertions(+), 12 deletions(-) create mode 100644 src/hooks.server.js diff --git a/scripts/redeploy.sh b/scripts/redeploy.sh index 16f3e11..25fa131 100644 --- a/scripts/redeploy.sh +++ b/scripts/redeploy.sh @@ -45,4 +45,4 @@ pm2 reload etc-prs echo "" success "Redeploy complete" divider -echo "" \ No newline at end of file +echo "" diff --git a/src/hooks.server.js b/src/hooks.server.js new file mode 100644 index 0000000..7c89495 --- /dev/null +++ b/src/hooks.server.js @@ -0,0 +1,39 @@ +/** @type {import('@sveltejs/kit').Handle} */ +export async function handle({ event, resolve }) { + const response = await resolve(event); + + // ── Security headers ──────────────────────────────────────────────────────── + // Prevent clickjacking + response.headers.set('X-Frame-Options', 'SAMEORIGIN'); + + // Prevent MIME-type sniffing + response.headers.set('X-Content-Type-Options', 'nosniff'); + + // Control referrer information + response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin'); + + // Restrict browser features + response.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()'); + + // Content Security Policy + // - default-src 'self': only load resources from our own origin + // - script-src 'self' 'unsafe-inline': SvelteKit needs inline scripts for hydration + // - style-src 'self' 'unsafe-inline' fonts.googleapis.com: inline styles + Google Fonts CSS + // - font-src 'self' fonts.gstatic.com: Google Fonts files + // - img-src 'self' data:: allow data URIs for any inline images + // - connect-src 'self': API calls only to our own origin + // - frame-ancestors 'none': belt-and-suspenders against clickjacking + response.headers.set('Content-Security-Policy', [ + "default-src 'self'", + "script-src 'self' 'unsafe-inline'", + "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com", + "font-src 'self' https://fonts.gstatic.com", + "img-src 'self' data:", + "connect-src 'self'", + "frame-ancestors 'none'", + "base-uri 'self'", + "form-action 'self'", + ].join('; ')); + + return response; +} diff --git a/src/routes/admin/+page.server.js b/src/routes/admin/+page.server.js index 38e714e..5e7693d 100644 --- a/src/routes/admin/+page.server.js +++ b/src/routes/admin/+page.server.js @@ -1,9 +1,14 @@ import { getAdminStats, listReports, listRecentPersonalitiesAdmin, listContactMessages } from '$lib/server/db.js'; export async function load({ url }) { - const reportFilter = url.searchParams.get('reports') ?? 'open'; - const messageFilter = url.searchParams.get('messages') ?? 'unread'; - const adminQ = url.searchParams.get('q') ?? ''; + const VALID_REPORT_FILTERS = ['open', 'dismissed', 'all']; + const VALID_MESSAGE_FILTERS = ['unread', 'all']; + + const reportFilter = VALID_REPORT_FILTERS.includes(url.searchParams.get('reports') ?? '') + ? url.searchParams.get('reports') : 'open'; + const messageFilter = VALID_MESSAGE_FILTERS.includes(url.searchParams.get('messages') ?? '') + ? url.searchParams.get('messages') : 'unread'; + const adminQ = (url.searchParams.get('q') ?? '').slice(0, 200); const adminPage = Math.max(1, parseInt(url.searchParams.get('page') ?? '1')); const stats = getAdminStats(); diff --git a/src/routes/admin/+page.svelte b/src/routes/admin/+page.svelte index 07ed56d..79ffa14 100644 --- a/src/routes/admin/+page.svelte +++ b/src/routes/admin/+page.svelte @@ -276,8 +276,7 @@ {msg.name || 'Anonymous'} {#if msg.email} - {msg.email} @@ -643,5 +642,3 @@ {/each} - {/if} - diff --git a/src/routes/library/+page.server.js b/src/routes/library/+page.server.js index f44ef88..7aa704f 100644 --- a/src/routes/library/+page.server.js +++ b/src/routes/library/+page.server.js @@ -2,13 +2,16 @@ import { listPersonalities, getDistinctManufacturers, getManufacturerCounts } fr import { MANUFACTURER_SEEDS } from '$lib/server/manufacturers.js'; const VALID_LIMITS = [12, 24, 48, 96]; +const VALID_SORTS = ['newest', 'popular']; +const MAX_Q_LEN = 200; export async function load({ url }) { - const q = url.searchParams.get('q') ?? ''; - const manufacturer = url.searchParams.get('manufacturer') ?? ''; - const sort = url.searchParams.get('sort') ?? 'newest'; + const q = (url.searchParams.get('q') ?? '').slice(0, MAX_Q_LEN); + const manufacturer = (url.searchParams.get('manufacturer') ?? '').slice(0, 128); + const sortParam = url.searchParams.get('sort') ?? 'newest'; + const sort = VALID_SORTS.includes(sortParam) ? sortParam : 'newest'; const page = Math.max(1, parseInt(url.searchParams.get('page') ?? '1')); - const view = url.searchParams.get('view') ?? 'cards'; + const view = url.searchParams.get('view') === 'table' ? 'table' : 'cards'; const limitParam = parseInt(url.searchParams.get('limit') ?? '24'); const limit = VALID_LIMITS.includes(limitParam) ? limitParam : 24;