diff --git a/dashcaddy-api/src/app.js b/dashcaddy-api/src/app.js index 4300983..1ad0f72 100644 --- a/dashcaddy-api/src/app.js +++ b/dashcaddy-api/src/app.js @@ -129,8 +129,12 @@ async function createApp() { }); // Disable x-powered-by header for security (don't advertise framework) app.disable('x-powered-by'); - // Trust first proxy (Caddy/nginx in front of us) so req.ip works correctly - app.set('trust proxy', 1); + // Trust reverse proxies on loopback AND Docker bridge networks so req.ip + // reflects the real client IP from X-Forwarded-For. 'loopback' covers + // bare-metal Caddy→node deployments; the Docker CIDRs cover containerised + // deployments where Caddy connects via the bridge gateway. External IPs + // cannot appear in this list, preventing X-Forwarded-For spoofing. + app.set('trust proxy', ['loopback', '172.16.0.0/12', '10.0.0.0/8']); // Initialize logging const log = createLogger(config.LOG_LEVEL); diff --git a/dashcaddy-api/src/utilities/middleware.js b/dashcaddy-api/src/utilities/middleware.js index 832254f..495195c 100644 --- a/dashcaddy-api/src/utilities/middleware.js +++ b/dashcaddy-api/src/utilities/middleware.js @@ -67,8 +67,11 @@ module.exports = function configureMiddleware(app, { crossOriginResourcePolicy: { policy: "cross-origin" } })); - // ── Trust proxy (one hop — Caddy) ── - app.set('trust proxy', 1); + // ── Trust proxy (loopback + Docker bridge) ── + // Only trust proxy headers from loopback and private network addresses. + // This prevents external IPs from spoofing X-Forwarded-For while + // supporting both bare-metal (Caddy on localhost) and Docker deployments. + app.set('trust proxy', ['loopback', '172.16.0.0/12', '10.0.0.0/8']); // ── JSON body parser (default 1MB limit) ── app.use(express.json({ limit: LIMITS.BODY_DEFAULT })); @@ -124,16 +127,14 @@ module.exports = function configureMiddleware(app, { } function extractTailscaleIPs(req) { + // req.ip is already correctly resolved by Express's trust-proxy setting. + // Only fall back to raw headers if req.ip is unavailable (e.g., before + // trust proxy is fully configured in edge-case setups). const clientIP = req.ip || req.socket?.remoteAddress || ''; - const forwardedFor = req.headers['x-forwarded-for']; - const realIP = req.headers['x-real-ip']; - const ipsToCheck = [clientIP, forwardedFor, realIP].filter(Boolean); - const fromTailscale = ipsToCheck.some(ip => - isTailscaleIP(ip.toString().split(',')[0].trim())); - const clientTailscaleIP = ipsToCheck - .map(ip => ip.toString().split(',')[0].trim()) - .find(ip => isTailscaleIP(ip)); - return { clientIP, ipsToCheck, fromTailscale, clientTailscaleIP }; + const clientTailscaleIP = isTailscaleIP(clientIP) ? clientIP : null; + const fromTailscale = clientTailscaleIP !== null; + + return { clientIP, ipsToCheck: [clientIP], fromTailscale, clientTailscaleIP }; } async function isIPInTailnet(clientTailscaleIP) {