import crypto from 'crypto' const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']) // Routes exempt from CSRF (external webhooks, magic link verify) const EXEMPT_PREFIXES = [ '/api/helcim/webhook', '/api/slack/webhook', '/api/auth/verify', '/oidc/', ] function isExempt(path) { return EXEMPT_PREFIXES.some(prefix => path.startsWith(prefix)) } export default defineEventHandler((event) => { const method = getMethod(event) const path = getRequestURL(event).pathname // Always set a CSRF token cookie if one doesn't exist let csrfToken = getCookie(event, 'csrf-token') if (!csrfToken) { csrfToken = crypto.randomBytes(32).toString('hex') setCookie(event, 'csrf-token', csrfToken, { httpOnly: false, // Must be readable by JS to include in requests secure: process.env.NODE_ENV === 'production', sameSite: 'lax', path: '/' }) } // Only check state-changing methods if (SAFE_METHODS.has(method)) return if (!path.startsWith('/api/')) return if (isExempt(path)) return // Double-submit cookie check: header must match cookie const headerToken = getHeader(event, 'x-csrf-token') if (!headerToken || headerToken !== csrfToken) { throw createError({ statusCode: 403, statusMessage: 'CSRF token missing or invalid' }) } })