Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place
96
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 126cd6931feee535a99acce5b847387c5d346d81 80 lines 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}