security updates

This commit is contained in:
RaineAllDay
2026-03-18 04:06:34 -06:00
parent 9722474440
commit 75d12b2950
5 changed files with 56 additions and 12 deletions

View File

@@ -45,4 +45,4 @@ pm2 reload etc-prs
echo ""
success "Redeploy complete"
divider
echo ""
echo ""

39
src/hooks.server.js Normal file
View File

@@ -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;
}

View File

@@ -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();

View File

@@ -276,8 +276,7 @@
{msg.name || 'Anonymous'}
</span>
{#if msg.email}
<a href="mailto:{msg.email}"
style="font-family:'DM Mono',monospace; font-size:11px; color:var(--cyan);
<a href="/cdn-cgi/l/email-protection#205b4d53470e454d41494c5d" style="font-family:'DM Mono',monospace; font-size:11px; color:var(--cyan);
text-decoration:none;">
{msg.email}
</a>
@@ -643,5 +642,3 @@
</button>
{/each}
</div>
{/if}
</div>

View File

@@ -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;