170 lines
5.4 KiB
TypeScript
170 lines
5.4 KiB
TypeScript
/**
|
|
* Issue #38: CSRF fails with multiple reverse proxies
|
|
*
|
|
* This test demonstrates how trust proxy settings affect CSRF validation
|
|
* when ExcaliDash is behind multiple proxy layers (e.g., Traefik, Synology NAS)
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
import express from "express";
|
|
import request from "supertest";
|
|
import {
|
|
createCsrfToken,
|
|
validateCsrfToken,
|
|
getCsrfTokenHeader,
|
|
} from "../security";
|
|
|
|
// mock the getClientId function behavior
|
|
const getClientIdFromRequest = (req: express.Request): string => {
|
|
const ip = req.ip || req.connection.remoteAddress || "unknown";
|
|
const userAgent = req.headers["user-agent"] || "unknown";
|
|
return `${ip}:${userAgent}`.slice(0, 256);
|
|
};
|
|
|
|
describe("Issue #38: CSRF with trust proxy settings", () => {
|
|
let app: express.Application;
|
|
|
|
beforeEach(() => {
|
|
app = express();
|
|
app.use(express.json());
|
|
});
|
|
|
|
it("demonstrates the trust proxy issue with multiple proxies", async () => {
|
|
// ext proxy -> frontend nginx -> backend
|
|
// X-Forwarded-For: 203.0.113.42 (client), 10.0.0.5 (external proxy), 172.17.0.3 (frontend nginx)
|
|
|
|
// With trust proxy: 1 (current setting)
|
|
const app1 = express();
|
|
app1.set("trust proxy", 1);
|
|
app1.use(express.json());
|
|
|
|
app1.get("/test-ip", (req, res) => {
|
|
res.json({
|
|
ip: req.ip,
|
|
clientId: getClientIdFromRequest(req),
|
|
});
|
|
});
|
|
|
|
// Simulate request through multiple proxies
|
|
const response1 = await request(app1)
|
|
.get("/test-ip")
|
|
.set("X-Forwarded-For", "203.0.113.42, 10.0.0.5, 172.17.0.3")
|
|
.set("User-Agent", "Mozilla/5.0 Test");
|
|
|
|
// With trust proxy: 1, Express takes second-to-last IP (the external proxy)
|
|
expect(response1.body.ip).toBe("10.0.0.5");
|
|
console.log(
|
|
"trust proxy: 1 → IP:",
|
|
response1.body.ip,
|
|
"(external proxy IP - WRONG)",
|
|
);
|
|
|
|
// With trust proxy: true
|
|
const app2 = express();
|
|
app2.set("trust proxy", true);
|
|
app2.use(express.json());
|
|
|
|
app2.get("/test-ip", (req, res) => {
|
|
res.json({
|
|
ip: req.ip,
|
|
clientId: getClientIdFromRequest(req),
|
|
});
|
|
});
|
|
|
|
const response2 = await request(app2)
|
|
.get("/test-ip")
|
|
.set("X-Forwarded-For", "203.0.113.42, 10.0.0.5, 172.17.0.3")
|
|
.set("User-Agent", "Mozilla/5.0 Test");
|
|
|
|
// With trust proxy: true, Express takes leftmost IP
|
|
expect(response2.body.ip).toBe("203.0.113.42");
|
|
console.log(
|
|
"trust proxy: true → IP:",
|
|
response2.body.ip,
|
|
"(real client IP - CORRECT)",
|
|
);
|
|
});
|
|
|
|
it("simulates CSRF failure scenario from issue #38", async () => {
|
|
const userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)";
|
|
|
|
// Request 1: Fetch CSRF token
|
|
// X-Forwarded-For shows: client, external-proxy-1, frontend-nginx
|
|
const clientIp1 = "203.0.113.42";
|
|
const externalProxyIp1 = "10.0.0.5"; // External proxy IP on first request
|
|
|
|
// With trust proxy: 1, Express sees the external proxy IP
|
|
const clientId1 = `${externalProxyIp1}:${userAgent}`;
|
|
const token = createCsrfToken(clientId1);
|
|
|
|
console.log(
|
|
" X-Forwarded-For:",
|
|
`${clientIp1}, ${externalProxyIp1}, 172.17.0.3`,
|
|
);
|
|
console.log(" Express sees IP:", externalProxyIp1);
|
|
console.log(" ClientId:", clientId1.slice(0, 50) + "...");
|
|
|
|
// Request 2: Try to create drawing with token
|
|
// External proxy IP might differ slightly
|
|
const externalProxyIp2 = "10.0.0.6";
|
|
|
|
const clientId2 = `${externalProxyIp2}:${userAgent}`;
|
|
|
|
console.log(
|
|
" X-Forwarded-For:",
|
|
`${clientIp1}, ${externalProxyIp2}, 172.17.0.3`,
|
|
);
|
|
console.log(" Express sees IP:", externalProxyIp2);
|
|
console.log(" ClientId:", clientId2.slice(0, 50) + "...");
|
|
|
|
// CSRF validation fails because clientId changed
|
|
const isValid = validateCsrfToken(clientId2, token);
|
|
|
|
expect(isValid).toBe(false);
|
|
console.log(" Expected:", clientId1.slice(0, 50) + "...");
|
|
console.log(" Got:", clientId2.slice(0, 50) + "...");
|
|
});
|
|
|
|
it("shows the fix works with trust proxy: true", async () => {
|
|
const userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)";
|
|
const realClientIp = "203.0.113.42";
|
|
|
|
const clientId1 = `${realClientIp}:${userAgent}`;
|
|
const token = createCsrfToken(clientId1);
|
|
|
|
console.log(" X-Forwarded-For:", `${realClientIp}, 10.0.0.5, 172.17.0.3`);
|
|
console.log(" Express sees IP:", realClientIp);
|
|
|
|
// Request 2: Use token (even if middle proxy IPs differ)
|
|
const clientId2 = `${realClientIp}:${userAgent}`;
|
|
|
|
console.log("Create drawing");
|
|
console.log("X-Forwarded-For:", `${realClientIp}, 10.0.0.6, 172.17.0.3`);
|
|
console.log("Express sees IP:", realClientIp, "(same!)");
|
|
|
|
const isValid = validateCsrfToken(clientId2, token);
|
|
|
|
expect(isValid).toBe(true);
|
|
console.log("\nCSRF Validation: SUCCESS");
|
|
});
|
|
|
|
it("demonstrates the Synology NAS scenario from issue #38", async () => {
|
|
const app = express();
|
|
app.set("trust proxy", 1);
|
|
app.use(express.json());
|
|
|
|
let seenIp: string | undefined;
|
|
app.get("/test", (req, res) => {
|
|
seenIp = req.ip;
|
|
res.json({ ip: req.ip });
|
|
});
|
|
|
|
// Client -> Synology (192.168.1.x) -> Docker frontend (192.168.11.x) -> Backend
|
|
await request(app)
|
|
.get("/test")
|
|
.set("X-Forwarded-For", "192.168.0.100, 192.168.1.4, 192.168.11.166");
|
|
console.log(" With trust proxy: 1, Express sees:", seenIp);
|
|
expect(seenIp).toBe("192.168.1.4"); // Proxy IP, not client IP
|
|
});
|
|
});
|