fix: multi-domain CSRF allowlist, trustProxy for nginx-ingress TLS termination

- hooks.server.ts: replace empty stub with multi-origin CSRF guard
  - Always allows humorunits.com and dickloads.com (+ www variants)
  - ALLOWED_ORIGINS env var for additional origins (staging, preview)
  - Dev: localhost:3000/4173/5173 auto-allowed when NODE_ENV != production
    or ALLOWED_ORIGINS is unset — no config needed for local dev
- svelte.config.js: disable built-in single-origin CSRF check (we own it now)
  trustProxy: true so X-Forwarded-Proto/IP are correct behind nginx-ingress
- login route: derive secure-cookie flag from X-Forwarded-Proto header so
  session cookies are marked Secure even when Node sees plain HTTP from ingress
- deployment.yaml: add NODE_ENV=production and explicit ALLOWED_ORIGINS
This commit is contained in:
Falkan
2026-03-19 13:18:21 -04:00
parent 8d2b838d84
commit 337b722cf2
4 changed files with 78 additions and 7 deletions

View File

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

View File

@@ -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<string> {
const origins = new Set<string>();
// 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);
};

View File

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

View File

@@ -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 }) =>