mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
fix(http): trust X-Real-IP only behind a declared proxy
The client-IP resolver read X-Real-IP on every path: with TRUST_PROXY_HOPS=0, which means no proxy in front and no forwarding header to believe, and on the path where the X-Forwarded-For chain was shorter than the declared hops, which means the proxies were bypassed. A caller sending a new X-Real-IP with each request got a new per-IP rate-limit bucket each time. X-Real-IP now gets the trust X-Forwarded-For gets: it is read only when at least one proxy hop is declared and the request carries no X-Forwarded-For. A short chain resolves to no address. The reverse-proxy guide no longer suggests TRUST_PROXY_HOPS=0 for a proxy that sets X-Real-IP; such a proxy takes 1.
This commit is contained in:
1 parent
92c5b0a60d
commit
bc69dec8e6
4 files changed
+50
-26
No files matched your search
+18
-12
@@ -390,9 +390,9 @@ export async function safeJson<T = unknown>(
|
||||
* IP-based rate-limits.
|
||||
*
|
||||
* Trust model (`TRUST_PROXY_HOPS` env):
|
||||
* - "0" → ignore XFF entirely, fall back to x-real-ip / null
|
||||
* - "0" → trust no forwarding header at all, XFF or x-real-ip
|
||||
* (use this if HealthLog is internet-facing without a
|
||||
* reverse proxy you control)
|
||||
* reverse proxy you control); the IP resolves to null
|
||||
* - "1" (default)→ trust exactly one hop (typical Coolify / Caddy /
|
||||
* Cloudflare-Tunnel single-proxy deployment); read the
|
||||
* rightmost XFF entry which is the IP your proxy
|
||||
@@ -540,18 +540,24 @@ export function getClientIpOrTrustWarning(request: Request): {
|
||||
// configured trust. Without this warning the silent degrade was
|
||||
// invisible until rate-limits visibly misfired in production.
|
||||
warnTrustViolationOnce(hops, chain.length);
|
||||
const realIp = request.headers.get("x-real-ip");
|
||||
return {
|
||||
ip: realIp && looksLikeIp(realIp) ? realIp : null,
|
||||
trustViolation: true,
|
||||
};
|
||||
// Not x-real-ip either. A chain shorter than the configured hops means
|
||||
// the request did not come through the proxies the operator declared,
|
||||
// so whatever x-real-ip it carries was set by the caller.
|
||||
return { ip: null, trustViolation: true };
|
||||
}
|
||||
// x-real-ip is a forwarding header like XFF and gets the same trust: only
|
||||
// when the operator declares a proxy in front (hops > 0), and only as the
|
||||
// single-proxy stand-in for a missing XFF. It used to be read on every
|
||||
// path, including `TRUST_PROXY_HOPS=0` ("no proxy, trust no header") and
|
||||
// the broken-chain path above, so a caller sending a fresh x-real-ip per
|
||||
// request got a fresh per-IP rate-limit bucket each time.
|
||||
const realIp = request.headers.get("x-real-ip")?.trim();
|
||||
return {
|
||||
ip: realIp && looksLikeIp(realIp) ? realIp : null,
|
||||
trustViolation: false,
|
||||
};
|
||||
}
|
||||
const realIp = request.headers.get("x-real-ip");
|
||||
return {
|
||||
ip: realIp && looksLikeIp(realIp) ? realIp : null,
|
||||
trustViolation: false,
|
||||
};
|
||||
return { ip: null, trustViolation: false };
|
||||
}
|
||||
|
||||
export function getClientIp(request: Request): string | null {
|
||||
|
||||
Reference in new issue
Block a user