mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
@@ -1,37 +1,59 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* 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.
|
||||
* Description: Three-layer server-side guard that blocks non-browser clients from reaching any API route.
|
||||
*
|
||||
* ── Layer 1 — Sec-Fetch-Site (ALL methods including GET) ────────────────────────────────────────
|
||||
* ── Layer 1 — Fetch Metadata family (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.
|
||||
* 1a) Sec-Fetch-Site presence + value
|
||||
* Browsers (Chrome 76+, Firefox 90+, Safari 16.4+) automatically attach Sec-Fetch-Site on
|
||||
* every request. It is a forbidden request header — JavaScript cannot set, override, or
|
||||
* remove it. Its absence reliably signals a non-browser client. The value cross-site is
|
||||
* also rejected; only same-origin, same-site, and none (direct navigation) are accepted.
|
||||
*
|
||||
* 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
|
||||
* Tools blocked (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) ───────────────────────────────
|
||||
* 1b) Sec-Fetch-Mode + Sec-Fetch-Dest presence + valid combination
|
||||
* Browsers that send Sec-Fetch-Site always send Mode and Dest too (Chrome 80+,
|
||||
* Firefox 90+, Safari 16.4+). Missing headers or impossible combinations signal
|
||||
* manual header injection. Only combinations expected on an API server are allowed:
|
||||
* cors|empty — standard fetch() call from a cross-origin SPA
|
||||
* same-origin|empty — same-origin fetch()
|
||||
* navigate|document — direct browser navigation to an API URL
|
||||
*
|
||||
* Additional tools blocked:
|
||||
* ✓ Scripts that fake only Sec-Fetch-Site — missing Mode or Dest
|
||||
* ✓ Scripts with wrong Mode+Dest combos — no-cors, cors+document, etc.
|
||||
*
|
||||
* ── Layer 2 — Browser presence signals (ALL methods including GET) ─────────────────────────────
|
||||
*
|
||||
* At least one browser-native header must be present:
|
||||
* Sec-CH-UA — Chromium client hint, forbidden in non-browser contexts
|
||||
* Accept-Language — sent by all browsers (Chrome, Firefox, Safari)
|
||||
* Absence of both is a strong automation signal that catches tools sophisticated enough to
|
||||
* replicate the Sec-Fetch-* family but not the full browser header profile.
|
||||
*
|
||||
* ── Layer 3 — 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.
|
||||
* even if an attacker replicated all browser headers above.
|
||||
*
|
||||
* ── 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.
|
||||
* ✗ Playwright / Puppeteer / Selenium controlling a real browser — they produce all correct
|
||||
* Sec-Fetch-* headers, Sec-CH-UA, and Accept-Language automatically.
|
||||
* ✗ A determined attacker who manually replicates all required headers.
|
||||
* The only defences at that point are rate limiting and valid credentials.
|
||||
*
|
||||
* ── Browser compatibility note ───────────────────────────────────────────────────────────────────
|
||||
*
|
||||
@@ -41,7 +63,7 @@
|
||||
*
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jun. 20, 2026
|
||||
* Date Modified: Jun. 20, 2026
|
||||
* Date Modified: Jun. 29, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
"use strict";
|
||||
|
||||
@@ -54,6 +76,13 @@ const ALLOWED = (process.env.ALLOWED_ORIGINS || process.env.APP_URL || '')
|
||||
|
||||
const MUTATION_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
||||
|
||||
// Valid Sec-Fetch-Mode + Sec-Fetch-Dest combinations expected on this API server.
|
||||
const VALID_FETCH_COMBOS = new Set([
|
||||
'cors|empty', // standard fetch() from cross-origin SPA
|
||||
'same-origin|empty', // same-origin fetch()
|
||||
'navigate|document', // direct browser navigation to an API URL
|
||||
]);
|
||||
|
||||
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.
|
||||
@@ -63,12 +92,25 @@ module.exports = function originGuard(req, res, 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']) {
|
||||
// ── Layer 1a: Sec-Fetch-Site must be present and not cross-site ───────────
|
||||
const fetchSite = req.headers['sec-fetch-site'];
|
||||
if (!fetchSite || fetchSite === 'cross-site') {
|
||||
return R.error(res, 'Forbidden.', 403);
|
||||
}
|
||||
|
||||
// ── Layer 2: Origin must be in allowlist for state-changing requests ──────
|
||||
// ── Layer 1b: Fetch Metadata family must be complete and form a valid combo ─
|
||||
const fetchMode = req.headers['sec-fetch-mode'];
|
||||
const fetchDest = req.headers['sec-fetch-dest'];
|
||||
if (!fetchMode || !fetchDest || !VALID_FETCH_COMBOS.has(`${fetchMode}|${fetchDest}`)) {
|
||||
return R.error(res, 'Forbidden.', 403);
|
||||
}
|
||||
|
||||
// ── Layer 2: At least one browser-native fingerprint header must be present ─
|
||||
if (!req.headers['sec-ch-ua'] && !req.headers['accept-language']) {
|
||||
return R.error(res, 'Forbidden.', 403);
|
||||
}
|
||||
|
||||
// ── Layer 3: 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)) {
|
||||
|
||||
@@ -80,10 +80,11 @@ const sensitiveOpsLimiter = rateLimit({
|
||||
message: { status: 'error', message: 'Too many sensitive operations. Please wait 1 hour.' },
|
||||
});
|
||||
|
||||
/** Admin routes — tighter than global */
|
||||
/** Admin routes — per authenticated user, not per IP */
|
||||
const adminLimiter = rateLimit({
|
||||
windowMs: windowMs15,
|
||||
max: 200,
|
||||
keyGenerator: (req) => req.user?.user_id?.toString() ?? req.ip,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
store: makeStore('admin'),
|
||||
|
||||
Reference in New Issue
Block a user