const { URL } = require('url'); /** * Anti-hotlinking middleware. Ensures that requests for assets originate * from the official app domain (SYSTEM_HOST). Blocks direct URL access. */ const verifyReferer = (req, res, next) => { // Cho phép các Bot của mạng xã hội truy cập để lấy ảnh thumbnail const userAgent = req.headers['user-agent'] || ''; const isSocialBot = /facebookexternalhit|Facebot|ZaloBot|Twitterbot|Slackbot|LinkedInBot|Embedly/i.test(userAgent); if (isSocialBot) { return next(); } const referer = req.headers.referer; const origin = req.headers.origin; // Prepare allowed origins for Referer/Origin check const primarySystemHost = process.env.SYSTEM_HOST || 'http://localhost:5000'; let configuredAllowedOrigins = []; // Add primary SYSTEM_HOST try { configuredAllowedOrigins.push(new URL(primarySystemHost).origin); } catch (e) { console.warn(`[Security Config Warning] Malformed SYSTEM_HOST: ${primarySystemHost}. Using as-is.`); configuredAllowedOrigins.push(primarySystemHost); } // Add additional allowed origins from environment variable (comma-separated) if (process.env.ADDITIONAL_ALLOWED_ORIGINS) { process.env.ADDITIONAL_ALLOWED_ORIGINS.split(',').forEach(originStr => { try { configuredAllowedOrigins.push(new URL(originStr.trim()).origin); } catch (e) { console.warn(`[Security Config Warning] Malformed origin in ADDITIONAL_ALLOWED_ORIGINS: ${originStr.trim()}. Skipping.`); } }); } const isMatch = (headerValue) => { if (!headerValue) return false; try { const urlObj = new URL(headerValue); const incomingOrigin = urlObj.origin; // Cho phép nếu khớp với bất kỳ origin nào trong danh sách cấu hình if (configuredAllowedOrigins.includes(incomingOrigin)) return true; // Trong môi trường development, cho phép localhost với bất kỳ port nào const isLocal = incomingOrigin.includes('localhost') || incomingOrigin.includes('127.0.0.1') || incomingOrigin.includes('::1'); if (process.env.NODE_ENV !== 'production' && isLocal) return true; return false; } catch (e) { console.warn(`[Security] Invalid URL in header value: ${headerValue}`); return false; } }; const hasValidReferer = isMatch(referer); const hasValidOrigin = isMatch(origin); // Block request if both referer and origin are missing or do not match SYSTEM_HOST if (!hasValidReferer && !hasValidOrigin) { if (process.env.NODE_ENV !== 'production') { console.warn(`[Security Blocked] Referer: ${referer || 'N/A'}, Origin: ${origin || 'N/A'}, Configured: ${configuredAllowedOrigins.join(', ')}`); } return res.status(403).json({ message: 'Access denied: Hotlinking detected or direct file access is prohibited.' }); } next(); }; /** * Cache prevention middleware. Ensures that sensitive image assets are never * cached by client browsers or intermediate proxies. */ const setNoCacheHeaders = (req, res, next) => { res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); res.setHeader('Pragma', 'no-cache'); res.setHeader('Expires', '0'); next(); }; module.exports = { verifyReferer, setNoCacheHeaders };