/*********************************************************************************************************************************************************************** * File Name: originGuard.middleware.js * Type of Program: Middleware * Description: Two-layer server-side guard that blocks non-browser clients from reaching any API route. * * ── Layer 1 — Sec-Fetch-Site (ALL methods including GET) ──────────────────────────────────────── * * Browsers (Chrome 76+, Firefox 90+, Safari 16.4+) automatically attach the Sec-Fetch-Site * header on every request. It is a forbidden request header — JavaScript cannot set, override, * or remove it. Its absence is a reliable, low-spoofability signal that the request originated * from a tool rather than a real browser. * * Tools blocked by this layer (default configurations): * ✓ Metasploit (Rex HTTP client) — no Sec-Fetch-Site * ✓ BurpSuite Repeater / Scanner — no Sec-Fetch-Site * ✓ Postman — no Sec-Fetch-Site * ✓ curl / wget / httpie / python-requests — no Sec-Fetch-Site * ✓ Nikto — no Sec-Fetch-Site * ✓ sqlmap — no Sec-Fetch-Site * ✓ dirb / gobuster / feroxbuster — no Sec-Fetch-Site * ✓ nmap HTTP scripts — no Sec-Fetch-Site * * ── Layer 2 — Origin allowlist (POST / PUT / PATCH / DELETE only) ─────────────────────────────── * * State-mutating requests must also carry an Origin header that matches ALLOWED_ORIGINS. * Stops credential-stuffing and cross-origin mutation attempts from unlisted domains, * even if an attacker managed to set Sec-Fetch-Site manually. * * ── What this does NOT stop ────────────────────────────────────────────────────────────────────── * * ✗ BurpSuite running as MITM proxy through a real browser session. * The browser supplies all correct headers — requests are indistinguishable from * legitimate traffic. The only defences here are rate limiting and valid credentials. * ✗ A determined attacker who manually replicates all browser headers in their tool. * * ── Browser compatibility note ─────────────────────────────────────────────────────────────────── * * Sec-Fetch-Site is supported by Chrome 76+ (Aug 2019), Firefox 90+ (Jul 2021), * and Safari 16.4+ (Mar 2023). Requests from browsers older than these thresholds * will be rejected. Given this is a modern SPA (Vite + React), this is acceptable. * * Author: Kenneth Obsequio (@lash0000) * Date Created: Jun. 20, 2026 * Date Modified: Jun. 20, 2026 ***********************************************************************************************************************************************************************/ "use strict"; const R = require('../utils/response.util'); const ALLOWED = (process.env.ALLOWED_ORIGINS || process.env.APP_URL || '') .split(',') .map(o => o.trim()) .filter(Boolean); const MUTATION_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']); module.exports = function originGuard(req, res, next) { // Dev bypass: set ORIGIN_GUARD_DISABLED=true in .env to allow Postman/curl through. // Hardcoded production lock — never bypassed even if the flag is accidentally set. if (process.env.ORIGIN_GUARD_DISABLED === 'true' && process.env.NODE_ENV !== 'production') { return next(); } if (req.method === 'OPTIONS') return next(); // preflight — handled by cors() // ── Layer 1: Sec-Fetch-Site must be present (covers GET scanning) ───────── if (!req.headers['sec-fetch-site']) { return R.error(res, 'Forbidden.', 403); } // ── Layer 2: Origin must be in allowlist for state-changing requests ────── if (MUTATION_METHODS.has(req.method)) { const origin = req.headers['origin']; if (!origin || !ALLOWED.includes(origin)) { return R.error(res, 'Forbidden.', 403); } } next(); };