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:
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user