diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml index 5b07294..e2bc99e 100644 --- a/k8s/deployment.yaml +++ b/k8s/deployment.yaml @@ -25,8 +25,12 @@ spec: value: "3000" - name: HOST value: "0.0.0.0" + - name: NODE_ENV + value: "production" - name: ORIGIN - value: "https://humorunits.com" # replace with your domain + value: "https://humorunits.com" + - name: ALLOWED_ORIGINS + value: "https://humorunits.com,https://www.humorunits.com,https://dickloads.com,https://www.dickloads.com" envFrom: - secretRef: name: humor-units-secrets # SESSION_SECRET, PASSWORD_HASH diff --git a/src/hooks.server.ts b/src/hooks.server.ts index fd70449..56b109f 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,4 +1,59 @@ -// hooks.server.ts intentionally minimal — CSRF is handled natively by SvelteKit -// when the Origin header matches the request Host. For multi-domain setups, -// trusted origins are declared in svelte.config.js via kit.csrf.checkOrigin / allowedOrigins. -export {}; +import { type Handle } from '@sveltejs/kit'; + +/** + * CSRF origin allowlist. + * + * Production: set ALLOWED_ORIGINS to a comma-separated list of trusted origins. + * e.g. ALLOWED_ORIGINS=https://humorunits.com,https://dickloads.com + * + * Dev: if ALLOWED_ORIGINS is unset (or NODE_ENV=development), localhost and + * local IPs are permitted automatically so `npm run dev` works without config. + */ +function buildAllowedOrigins(): Set { + const origins = new Set(); + + // Always allow the two production domains + origins.add('https://humorunits.com'); + origins.add('https://www.humorunits.com'); + origins.add('https://dickloads.com'); + origins.add('https://www.dickloads.com'); + + // Additional origins from env (e.g. staging, preview URLs) + const extra = process.env.ALLOWED_ORIGINS; + if (extra) { + for (const o of extra.split(',')) { + const trimmed = o.trim(); + if (trimmed) origins.add(trimmed); + } + } + + // In development (or when no explicit origins override is set), allow localhost + if (process.env.NODE_ENV !== 'production' || !extra) { + origins.add('http://localhost'); + // Allow any localhost port + for (const port of [3000, 4173, 5173]) { + origins.add(`http://localhost:${port}`); + } + } + + return origins; +} + +const ALLOWED_ORIGINS = buildAllowedOrigins(); + +export const handle: Handle = async ({ event, resolve }) => { + const { request } = event; + const isMutating = !['GET', 'HEAD', 'OPTIONS'].includes(request.method); + + if (isMutating) { + const origin = request.headers.get('origin'); + // If Origin header is present, it must be in the allowlist. + // Absence of Origin (e.g. same-origin non-browser requests) is allowed through. + if (origin && !ALLOWED_ORIGINS.has(origin)) { + console.warn(`[humor-units] CSRF: blocked origin "${origin}"`); + return new Response('Forbidden', { status: 403 }); + } + } + + return resolve(event); +}; diff --git a/src/routes/admin/api/auth/login/+server.ts b/src/routes/admin/api/auth/login/+server.ts index 03486e9..1eaa8f5 100644 --- a/src/routes/admin/api/auth/login/+server.ts +++ b/src/routes/admin/api/auth/login/+server.ts @@ -30,5 +30,8 @@ export const POST: RequestHandler = async ({ request, url }) => { } const token = createSession(config.sessionSecret); - return sessionResponse(token, url.protocol === 'https:', COOKIE_MAX_AGE); + // Behind nginx-ingress, TLS terminates at the proxy and Node sees plain HTTP. + // X-Forwarded-Proto carries the original scheme; fall back to url.protocol for dev. + const proto = request.headers.get('x-forwarded-proto') ?? url.protocol.replace(':', ''); + return sessionResponse(token, proto === 'https', COOKIE_MAX_AGE); }; diff --git a/svelte.config.js b/svelte.config.js index 6423e6e..5306bd8 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -3,7 +3,16 @@ import adapter from '@sveltejs/adapter-node'; /** @type {import('@sveltejs/kit').Config} */ const config = { kit: { - adapter: adapter(), + adapter: adapter({ + // Trust X-Forwarded-Proto / X-Forwarded-For from the nginx-ingress. + // Required so that `url.protocol` and client IP are correct behind the proxy. + trustProxy: true, + }), + // Disable SvelteKit's built-in single-origin CSRF check — we handle it + // ourselves in hooks.server.ts with a multi-origin allowlist. + csrf: { + checkOrigin: false, + }, }, vitePlugin: { dynamicCompileOptions: ({ filename }) =>