Replace SvelteKit CSRF with custom multi-origin check
Deploy to LXC / deploy (push) Successful in 19s

SvelteKit's built-in CSRF only allows one origin, breaking access via
NetBird/Yggdrasil/Tor IPs. Now:
- Disabled checkOrigin in svelte.config.js
- Custom CSRF in hooks.server.ts checks Origin against ALLOWED_ORIGINS
- ALLOWED_ORIGINS env var: comma-separated list of trusted origins
- Caddy no longer needs to rewrite Host/Origin headers
- Each access method (public domain, NetBird IP, Yggdrasil, Tor onion)
  just needs its URL added to ALLOWED_ORIGINS

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-13 14:23:02 +07:00
co-authored by Claude Opus 4.6
parent dbc140c1f9
commit 0ceee55f9b
4 changed files with 38 additions and 9 deletions
+30 -5
View File
@@ -1,21 +1,47 @@
import type { Handle, HandleServerError } from '@sveltejs/kit';
import { validateSession, setSessionCookie, deleteSessionCookie } from '$lib/server/auth/index.js';
import { env } from '$env/dynamic/private';
import { error } from '@sveltejs/kit';
export const handleError: HandleServerError = async ({ error }) => {
const message = error instanceof Error ? error.message : 'Unknown error';
export const handleError: HandleServerError = async ({ error: err }) => {
const message = err instanceof Error ? err.message : 'Unknown error';
// Body size limit exceeded
if (message.includes('exceeds limit')) {
return {
message: 'File too large. Maximum upload size is 50MB.'
};
}
console.error('Unhandled error:', error);
console.error('Unhandled error:', err);
return { message: 'An unexpected error occurred.' };
};
// Trusted origins for CSRF — set ALLOWED_ORIGINS in .env as comma-separated list
// e.g. ALLOWED_ORIGINS=https://collection.newedge.house,http://100.81.174.129
function getAllowedOrigins(): Set<string> {
const origins = new Set<string>();
if (env.ORIGIN) origins.add(env.ORIGIN);
if (env.BASE_URL) origins.add(env.BASE_URL);
const extra = env.ALLOWED_ORIGINS?.split(',').map((o) => o.trim()).filter(Boolean);
if (extra) for (const o of extra) origins.add(o);
return origins;
}
export const handle: Handle = async ({ event, resolve }) => {
// CSRF check for non-GET requests
if (event.request.method !== 'GET' && event.request.method !== 'HEAD') {
const origin = event.request.headers.get('origin');
if (origin) {
const allowed = getAllowedOrigins();
if (allowed.size > 0 && !allowed.has(origin)) {
error(403, 'Cross-site request blocked');
}
}
}
const token = event.cookies.get('session');
if (token) {
@@ -25,7 +51,6 @@ export const handle: Handle = async ({ event, resolve }) => {
event.locals.user = user;
event.locals.session = session;
// Refresh cookie if session was extended
if (session.fresh) {
setSessionCookie(event, token, session.expiresAt);
}