From f9f8ac953e4a6b9bfc7fbbba2160bc4a27a75e9b Mon Sep 17 00:00:00 2001 From: Kenneth Obsequio Date: Tue, 14 Jul 2026 18:30:00 +0800 Subject: [PATCH] middleware do not prohibits Signed-off-by: Kenneth Obsequio --- middleware/originGuard.middleware.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/middleware/originGuard.middleware.js b/middleware/originGuard.middleware.js index a99516f..c638185 100644 --- a/middleware/originGuard.middleware.js +++ b/middleware/originGuard.middleware.js @@ -76,6 +76,13 @@ const ALLOWED = (process.env.ALLOWED_ORIGINS || process.env.APP_URL || '') const MUTATION_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']); +// Routes that legitimately receive a top-level cross-site navigation — a third-party +// IdP (Google) redirects the browser here after consent, so Sec-Fetch-Site is +// correctly "cross-site" even though the request is a real browser, not an attacker. +// Still gated on GET + navigate|document below, so this doesn't open the route to +// cross-site fetch()/XHR — only actual browser navigations. +const CROSS_SITE_NAVIGATION_PATHS = new Set(['/google/callback']); + // 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 @@ -93,8 +100,12 @@ module.exports = function originGuard(req, res, next) { if (req.method === 'OPTIONS') return next(); // preflight — handled by cors() // ── Layer 1a: Sec-Fetch-Site must be present and not cross-site ─────────── + // Exception: OAuth callback routes receive a real cross-site top-level navigation + // from the IdP's domain — allowed only in combination with the navigate|document + // check in Layer 1b, so cross-site fetch()/XHR is still rejected everywhere. const fetchSite = req.headers['sec-fetch-site']; - if (!fetchSite || fetchSite === 'cross-site') { + const isCrossSiteNavigationRoute = req.method === 'GET' && CROSS_SITE_NAVIGATION_PATHS.has(req.path); + if (!fetchSite || (fetchSite === 'cross-site' && !isCrossSiteNavigationRoute)) { return R.error(res, 'Forbidden.', 403); } @@ -104,6 +115,9 @@ module.exports = function originGuard(req, res, next) { if (!fetchMode || !fetchDest || !VALID_FETCH_COMBOS.has(`${fetchMode}|${fetchDest}`)) { return R.error(res, 'Forbidden.', 403); } + if (isCrossSiteNavigationRoute && `${fetchMode}|${fetchDest}` !== 'navigate|document') { + 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']) {