Replace SvelteKit CSRF with custom multi-origin check
Deploy to LXC / deploy (push) Successful in 19s
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:
@@ -1,4 +1,6 @@
|
|||||||
DATABASE_URL=postgresql://bflr:bflr_dev@localhost:5432/buildfor_life_repair
|
DATABASE_URL=postgresql://bflr:bflr_dev@localhost:5432/buildfor_life_repair
|
||||||
UPLOAD_DIR=static/uploads
|
UPLOAD_DIR=static/uploads
|
||||||
BASE_URL=http://localhost:5173
|
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
|
BODY_SIZE_LIMIT=52428800
|
||||||
|
|||||||
@@ -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
|
## 1. Install Caddy and Tor
|
||||||
|
|
||||||
@@ -75,8 +75,6 @@ Edit `/etc/caddy/Caddyfile`:
|
|||||||
|
|
||||||
(proxy) {
|
(proxy) {
|
||||||
reverse_proxy 127.0.0.1:3000 {
|
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-Real-IP {remote_host}
|
||||||
header_up X-Forwarded-For {remote_host}
|
header_up X-Forwarded-For {remote_host}
|
||||||
header_up X-Forwarded-Proto https
|
header_up X-Forwarded-Proto https
|
||||||
@@ -164,6 +162,7 @@ HOST=127.0.0.1
|
|||||||
PORT=3000
|
PORT=3000
|
||||||
ORIGIN=https://collection.newedge.house
|
ORIGIN=https://collection.newedge.house
|
||||||
BASE_URL=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:
|
systemd service:
|
||||||
|
|||||||
+30
-5
@@ -1,21 +1,47 @@
|
|||||||
import type { Handle, HandleServerError } from '@sveltejs/kit';
|
import type { Handle, HandleServerError } from '@sveltejs/kit';
|
||||||
import { validateSession, setSessionCookie, deleteSessionCookie } from '$lib/server/auth/index.js';
|
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 }) => {
|
export const handleError: HandleServerError = async ({ error: err }) => {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||||
|
|
||||||
// Body size limit exceeded
|
|
||||||
if (message.includes('exceeds limit')) {
|
if (message.includes('exceeds limit')) {
|
||||||
return {
|
return {
|
||||||
message: 'File too large. Maximum upload size is 50MB.'
|
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.' };
|
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 }) => {
|
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');
|
const token = event.cookies.get('session');
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
@@ -25,7 +51,6 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|||||||
event.locals.user = user;
|
event.locals.user = user;
|
||||||
event.locals.session = session;
|
event.locals.session = session;
|
||||||
|
|
||||||
// Refresh cookie if session was extended
|
|
||||||
if (session.fresh) {
|
if (session.fresh) {
|
||||||
setSessionCookie(event, token, session.expiresAt);
|
setSessionCookie(event, token, session.expiresAt);
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -15,7 +15,10 @@ const config = {
|
|||||||
adapter: adapter({
|
adapter: adapter({
|
||||||
out: 'build',
|
out: 'build',
|
||||||
precompress: true
|
precompress: true
|
||||||
})
|
}),
|
||||||
|
csrf: {
|
||||||
|
checkOrigin: false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user