[grade=B] fix: harden trust-proxy setting + eliminate raw X-Forwarded-For spoofing
Codex previously graded D (blocking) for two security concerns: 1. trust proxy:1 trusts any direct connection as a proxy 2. extractTailscaleIPs reads raw X-Forwarded-For/X-Real-IP headers Fixes: - trust proxy changed from '1' to ['loopback', '172.16.0.0/12', '10.0.0.0/8'] - Covers bare-metal (Caddy on localhost) and Docker bridge deployments - External IPs cannot appear in list → cannot spoof X-Forwarded-For - extractTailscaleIPs now uses req.ip (resolved by Express trust-proxy chain) instead of reading raw headers directly - Codex re-grade: B (no blocking issues) - All 1775 tests pass Security verified on test server: - Sensitive routes: 403 from external ✓ - Public routes: 200 ✓ - Health endpoints: JSON 200 ✓
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user