Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place
at main 2.1 kB view raw
1import { Elysia } from 'elysia' 2import { logger } from './logger' 3 4/** 5 * CSRF Protection using Origin/Host header verification 6 * Based on Lucia's recommended approach for cookie-based authentication 7 * 8 * This validates that the Origin header matches the Host header for 9 * state-changing requests (POST, PUT, DELETE, PATCH). 10 */ 11 12/** 13 * Verify that the request origin matches the expected host 14 * @param origin - The Origin header value 15 * @param allowedHosts - Array of allowed host values 16 * @returns true if origin is valid, false otherwise 17 */ 18export function verifyRequestOrigin(origin: string, allowedHosts: string[]): boolean { 19 if (!origin) { 20 return false 21 } 22 23 try { 24 const originUrl = new URL(origin) 25 const originHost = originUrl.host 26 27 return allowedHosts.some(host => originHost === host) 28 } catch { 29 // Invalid URL 30 return false 31 } 32} 33 34/** 35 * CSRF Protection Middleware for Elysia 36 * 37 * Validates Origin header against Host header for non-GET requests 38 * to prevent CSRF attacks when using cookie-based authentication. 39 * 40 * Usage: 41 * ```ts 42 * import { csrfProtection } from './lib/csrf' 43 * 44 * new Elysia() 45 * .use(csrfProtection()) 46 * .post('/api/protected', handler) 47 * ``` 48 */ 49export const csrfProtection = () => { 50 return new Elysia({ name: 'csrf-protection' }) 51 .onBeforeHandle(({ request, set }) => { 52 const method = request.method.toUpperCase() 53 54 // Only protect state-changing methods 55 if (['GET', 'HEAD', 'OPTIONS'].includes(method)) { 56 return 57 } 58 59 // Get headers 60 const originHeader = request.headers.get('Origin') 61 // Use X-Forwarded-Host if behind a proxy, otherwise use Host 62 const hostHeader = request.headers.get('X-Forwarded-Host') || request.headers.get('Host') 63 64 // Validate origin matches host 65 if (!originHeader || !hostHeader || !verifyRequestOrigin(originHeader, [hostHeader])) { 66 logger.warn('[CSRF] Request blocked', { 67 method, 68 origin: originHeader, 69 host: hostHeader, 70 path: new URL(request.url).pathname 71 }) 72 73 set.status = 403 74 return { 75 error: 'CSRF validation failed', 76 message: 'Request origin does not match host' 77 } 78 } 79 }) 80}