/** * CSRF protection and security utilities for the API server. * * Security model: * - This is a loopback-only server (127.0.0.1) * - CSRF protection uses Origin header validation * - Requests WITHOUT Origin header are allowed (same-origin browser requests, curl) * - Setting GNO_API_TOKEN enables token auth as an alternative to Origin validation * but does NOT require it for non-browser clients (they just omit Origin) * * @module src/serve/security */ /** * Validate Origin header for CSRF protection. * Same-origin requests (no Origin header) are allowed - this includes: * - Same-origin browser fetch/XHR (browser omits Origin for same-origin) * - curl and other non-browser clients (no Origin header by default) * Cross-origin browser requests must originate from localhost/127.0.0.1. */ export function validateOrigin(req: Request, port: number): boolean { const origin = req.headers.get("Origin"); // Same-origin requests (browser fetch, curl) have no Origin header if (!origin) { return true; } // For port 0 (ephemeral), we can't validate specific port - only allow if // the port in the request matches the expected allowed origins pattern const allowed = port === 0 ? [] // Can't know actual port, reject cross-origin for ephemeral : [`http://localhost:${port}`, `http://127.0.0.1:${port}`]; return allowed.includes(origin); } /** * Validate API token for non-browser clients (e.g., Raycast). * Token auth is optional - only enabled if GNO_API_TOKEN env var is set. * When enabled, requests with valid token bypass Origin validation. * When disabled (no env var), this always returns false. */ export function validateToken(req: Request): boolean { const expectedToken = process.env.GNO_API_TOKEN; // Token auth disabled if env var not set if (!expectedToken) { return false; } const token = req.headers.get("X-GNO-Token"); return token === expectedToken; } /** * Check if request is allowed (CSRF protection). * GET/HEAD/OPTIONS are always allowed (safe methods). * POST/PUT/DELETE require either: * - Valid Origin header (localhost/127.0.0.1 on same port), OR * - No Origin header (same-origin browser or non-browser client), OR * - Valid X-GNO-Token header (if GNO_API_TOKEN env is set) */ export function isRequestAllowed(req: Request, port: number): boolean { const method = req.method.toUpperCase(); // Safe methods - no CSRF protection needed if (method === "GET" || method === "HEAD" || method === "OPTIONS") { return true; } // Unsafe methods - require valid Origin or token return validateToken(req) || validateOrigin(req, port); } /** * Create 403 Forbidden response for CSRF violations. * Uses same error envelope as other API errors for consistency. */ export function forbiddenResponse(): Response { return Response.json( { error: { code: "CSRF_VIOLATION", message: "Forbidden" } }, { status: 403 } ); }