/** * @file same-origin.js * @description Loopback-Origin guard for the routes that act on the machine * rather than on the dashboard's own database — spawning a process, or * writing a file into a directory the request names. * * The dashboard binds to localhost, so any page in the user's browser can * reach it. Without this, a malicious site could POST to it and, say, drop a * CLAUDE.md full of instructions into a project the user later opens. * * Browser requests carry `Origin`; it must be a loopback host. Requests with * no `Origin` (curl, the CLI) pass — they are not the drive-by case. `Referer` * is the fallback for older browsers. * * Extracted from routes/run.js so the config routes enforce the identical * rule instead of a second, drifting copy. * * @author Nguyễn Ngọc Trí Vĩ */ const ALLOWED_ORIGIN_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "0.0.0.0"]); function isLoopback(raw) { try { return ALLOWED_ORIGIN_HOSTS.has(new URL(raw).hostname); } catch { return false; } } function sameOriginGuard(req, res, next) { const origin = req.headers.origin; if (origin) { if (!isLoopback(origin)) { return res.status(403).json({ error: { code: "EBADORIGIN", message: "cross-origin requests are not allowed" }, }); } return next(); } const referer = req.headers.referer; if (referer && !isLoopback(referer)) { return res.status(403).json({ error: { code: "EBADORIGIN", message: "cross-origin requests are not allowed" }, }); } return next(); } module.exports = { sameOriginGuard, ALLOWED_ORIGIN_HOSTS };