From 0ceee55f9b2c62052ee1be7d89726d2dae930602 Mon Sep 17 00:00:00 2001 From: grabowski Date: Mon, 13 Apr 2026 14:23:02 +0700 Subject: [PATCH] Replace SvelteKit CSRF with custom multi-origin check 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) --- .env.example | 2 ++ docs/caddy-reverse-proxy.md | 5 ++--- src/hooks.server.ts | 35 ++++++++++++++++++++++++++++++----- svelte.config.js | 5 ++++- 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/.env.example b/.env.example index 5af6bb3..13e9c76 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,6 @@ DATABASE_URL=postgresql://bflr:bflr_dev@localhost:5432/buildfor_life_repair UPLOAD_DIR=static/uploads BASE_URL=http://localhost:5173 +ORIGIN=https://collection.newedge.house +ALLOWED_ORIGINS=https://collection.newedge.house,http://100.81.174.129 BODY_SIZE_LIMIT=52428800 diff --git a/docs/caddy-reverse-proxy.md b/docs/caddy-reverse-proxy.md index 46b0771..33f3641 100644 --- a/docs/caddy-reverse-proxy.md +++ b/docs/caddy-reverse-proxy.md @@ -19,7 +19,7 @@ Tor ──► .onion ──► tor ──► :8880 ──┤ └─────────────────────────────────┘ ``` -All routes inject `Host: collection.newedge.house` so SvelteKit CSRF passes. +CSRF is handled by the app via `ALLOWED_ORIGINS` env var — no header rewriting needed. ## 1. Install Caddy and Tor @@ -75,8 +75,6 @@ Edit `/etc/caddy/Caddyfile`: (proxy) { reverse_proxy 127.0.0.1:3000 { - header_up Host collection.newedge.house - header_up Origin https://collection.newedge.house header_up X-Real-IP {remote_host} header_up X-Forwarded-For {remote_host} header_up X-Forwarded-Proto https @@ -164,6 +162,7 @@ HOST=127.0.0.1 PORT=3000 ORIGIN=https://collection.newedge.house BASE_URL=https://collection.newedge.house +ALLOWED_ORIGINS=https://collection.newedge.house,http://100.x.x.x,http://[200:xxxx:...],http://your-onion.onion ``` systemd service: diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 007503f..463f651 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -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 { + const origins = new Set(); + + 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); } diff --git a/svelte.config.js b/svelte.config.js index e6d4807..9772cee 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -15,7 +15,10 @@ const config = { adapter: adapter({ out: 'build', precompress: true - }) + }), + csrf: { + checkOrigin: false + } } };