diff --git a/packages/server/src/ratelimit.ts b/packages/server/src/ratelimit.ts index d48001f..3e38e87 100644 --- a/packages/server/src/ratelimit.ts +++ b/packages/server/src/ratelimit.ts @@ -31,10 +31,17 @@ export class SlidingLimit { } } -/** The client's address as Caddy reports it — the first hop in - * X-Forwarded-For — falling back to the socket's own peer. */ +/** The client's address as Caddy reports it. The proxy APPENDS the true + * peer to X-Forwarded-For, so the last entry is the trustworthy one; a + * client can write anything into the first. And the header is believed + * only when the socket peer is the proxy itself (loopback) — reached + * any other way, the peer address is the client. */ export function clientAddress(headers: Record, remote: string | undefined): string { + const peer = remote || "unknown"; + if (!LOOPBACK.has(peer)) return peer; const fwd = headers["x-forwarded-for"]; - const first = (Array.isArray(fwd) ? fwd[0] : fwd)?.split(",")[0]?.trim(); - return first || remote || "unknown"; + const parts = (Array.isArray(fwd) ? fwd.join(",") : fwd ?? "").split(",").map((s) => s.trim()).filter(Boolean); + return parts[parts.length - 1] || peer; } + +const LOOPBACK = new Set(["127.0.0.1", "::1", "::ffff:127.0.0.1"]);