import type { Request, Response, NextFunction } from 'express' const MUTATING_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']) const SESSION_COOKIE_PATTERN = /(?:^|;\s*)(?:admin_session|employee_session|renter_session)=/ function configuredOrigins() { return [ process.env.DASHBOARD_URL, process.env.ADMIN_URL, process.env.CARPLACE_URL, process.env.WEBSITE_URL, process.env.NEXT_PUBLIC_DASHBOARD_URL, process.env.NEXT_PUBLIC_ADMIN_URL, process.env.NEXT_PUBLIC_CARPLACE_URL, process.env.NEXT_PUBLIC_WEBSITE_URL, process.env.CORS_ORIGINS, ] .flatMap((value) => (value ?? '').split(',')) .map((value) => value.trim()) .filter(Boolean) } function normalizeOrigin(value: string | undefined): string | null { if (!value) return null try { const url = new URL(value) return url.origin } catch { return null } } function originFromReferer(value: string | undefined): string | null { if (!value) return null try { return new URL(value).origin } catch { return null } } function isAllowedDevelopmentOrigin(origin: string) { if (process.env.NODE_ENV === 'production') return false try { const url = new URL(origin) if (url.protocol !== 'http:') return false const trustedDevPorts = new Set(['3000', '3001', '3002', '3004', '4000']) if (!trustedDevPorts.has(url.port)) return false if (['localhost', '127.0.0.1'].includes(url.hostname)) return true const octets = url.hostname.split('.').map((part) => Number(part)) if (octets.length !== 4 || octets.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) { return false } const first = octets[0]! const second = octets[1]! return first === 10 || (first === 172 && second >= 16 && second <= 31) || (first === 192 && second === 168) } catch { return false } } export function isTrustedBrowserOrigin(origin: string | null) { if (!origin) return false const allowed = new Set(configuredOrigins().map(normalizeOrigin).filter((value): value is string => Boolean(value))) return allowed.has(origin) || isAllowedDevelopmentOrigin(origin) } function isCookieAuthenticatedBrowserMutation(req: Request) { if (!MUTATING_METHODS.has(req.method.toUpperCase())) return false const cookie = req.headers.cookie ?? '' if (!SESSION_COOKIE_PATTERN.test(cookie)) return false const secFetchSite = Array.isArray(req.headers['sec-fetch-site']) ? req.headers['sec-fetch-site'][0] : req.headers['sec-fetch-site'] if (secFetchSite && secFetchSite !== 'same-origin' && secFetchSite !== 'same-site' && secFetchSite !== 'none') return true // Browser cookie-authenticated mutations must present Origin. Referer is a // fallback for older clients only; API clients should use Bearer tokens. return true } export function requireTrustedOriginForCookieMutations(req: Request, res: Response, next: NextFunction) { if (!isCookieAuthenticatedBrowserMutation(req)) return next() const origin = normalizeOrigin(req.headers.origin as string | undefined) ?? originFromReferer(req.headers.referer as string | undefined) if (!isTrustedBrowserOrigin(origin)) { return res.status(403).json({ error: 'csrf_origin_rejected', message: 'Mutating cookie-authenticated requests must come from a trusted application origin.', statusCode: 403, }) } next() }