The address limits believe the proxy, not the client

X-Forwarded-For's first entry is whatever the client wrote; Caddy
appends the true peer last. The limiter now reads the last entry, and
only when the socket peer is loopback — the proxy — so a request that
somehow reaches the server directly is keyed by its own address.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
Eric Wagoner
2026-09-03 11:32:21 -04:00
co-authored by Claude Fable 5.1
parent ca108f83a6
commit ddd2e347bd
+11 -4
View File
@@ -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<string, string | string[] | undefined>, 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"]);