diff --git a/backend/src/__tests__/csrf-trust-proxy.test.ts b/backend/src/__tests__/csrf-trust-proxy.test.ts index 3bdf634..021994f 100644 --- a/backend/src/__tests__/csrf-trust-proxy.test.ts +++ b/backend/src/__tests__/csrf-trust-proxy.test.ts @@ -97,7 +97,6 @@ describe("Issue #38: CSRF with trust proxy settings", () => { const clientId1 = `${externalProxyIp1}:${userAgent}`; const token = createCsrfToken(clientId1); - console.log("\nšŸ“ Step 1: Client fetches CSRF token"); console.log( " X-Forwarded-For:", `${clientIp1}, ${externalProxyIp1}, 172.17.0.3`, @@ -111,7 +110,6 @@ describe("Issue #38: CSRF with trust proxy settings", () => { const clientId2 = `${externalProxyIp2}:${userAgent}`; - console.log("\nšŸ“¤ Step 2: Client creates drawing with token"); console.log( " X-Forwarded-For:", `${clientIp1}, ${externalProxyIp2}, 172.17.0.3`, diff --git a/backend/src/index.ts b/backend/src/index.ts index 0a4a773..65adce9 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -132,8 +132,21 @@ const app = express(); // Trust proxy headers (X-Forwarded-For, X-Real-IP) from nginx // Required for correct client IP detection when running behind a reverse proxy -// This fixes CSRF token validation failures in Docker/K8s environments -app.set("trust proxy", 1); +// Fix for issue #38: Use 'true' to handle multiple proxy layers (e.g., Traefik, Synology NAS) +// This ensures Express extracts the real client IP from the leftmost X-Forwarded-For value +const trustProxyConfig = process.env.TRUST_PROXY || "true"; +const trustProxyValue = trustProxyConfig === "true" + ? true + : trustProxyConfig === "false" + ? false + : parseInt(trustProxyConfig, 10) || 1; +app.set("trust proxy", trustProxyValue); + +if (trustProxyValue === true) { + console.log("[config] trust proxy: enabled (handles multiple proxy layers)"); +} else { + console.log(`[config] trust proxy: ${trustProxyValue}`); +} const httpServer = createServer(app); const io = new Server(httpServer, { @@ -330,9 +343,24 @@ app.use((req, res, next) => { const getClientId = (req: express.Request): string => { const ip = req.ip || req.connection.remoteAddress || "unknown"; const userAgent = req.headers["user-agent"] || "unknown"; - // Create a simple hash for client identification - // In production, you might use a session ID instead - return `${ip}:${userAgent}`.slice(0, 256); + const clientId = `${ip}:${userAgent}`.slice(0, 256); + + // Debug logging for CSRF troubleshooting (issue #38) + if (process.env.DEBUG_CSRF === "true") { + console.log("[CSRF DEBUG] getClientId", { + method: req.method, + path: req.path, + ip, + remoteAddress: req.connection.remoteAddress, + "x-forwarded-for": req.headers["x-forwarded-for"], + "x-real-ip": req.headers["x-real-ip"], + userAgent: userAgent.slice(0, 100), + clientIdPreview: clientId.slice(0, 60) + "...", + trustProxySetting: req.app.get("trust proxy"), + }); + } + + return clientId; }; // Rate limiter specifically for CSRF token generation to prevent store exhaustion diff --git a/backend/src/security.ts b/backend/src/security.ts index 87287ed..bd90c33 100644 --- a/backend/src/security.ts +++ b/backend/src/security.ts @@ -569,8 +569,12 @@ const getCsrfSecret = (): Buffer => { cachedCsrfSecret = crypto.randomBytes(32); const envLabel = process.env.NODE_ENV ? ` (${process.env.NODE_ENV})` : ""; console.warn( - `[security] CSRF_SECRET is not set${envLabel}. Using an ephemeral per-process secret. ` + - "For horizontal scaling (k8s), set CSRF_SECRET to the same value on all instances." + `[SECURITY WARNING] CSRF_SECRET is not set${envLabel}.\n` + + `Using an ephemeral per-process secret.\n` + + ` - Tokens will expire on container restart\n` + + ` - Horizontal scaling (k8s) will NOT work\n` + + ` - Generate a secret: openssl rand -base64 32\n` + + ` - Set environment variable: CSRF_SECRET=` ); return cachedCsrfSecret; };