mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
fix(http): read forwarding headers only from a connection a proxy could make
The client-IP resolver trusted one forwarding hop by default and read the rightmost X-Forwarded-For entry. Next fills that header with the socket's address only when the request carries none, so a caller reaching the published port directly could send its own and choose a fresh address per request: every per-IP limit (sign-in, second factor, passkey, share unlock, webhooks) became per-request, and sessions, the audit log and sign-in notices recorded the chosen address. The server now writes each connection's own peer address into an internal header from Node's http.server.request.start diagnostics channel, installed at boot and overwriting anything a caller sent under that name. The resolver reads forwarding headers only when that peer is loopback, private, unique-local or CGNAT, where a reverse proxy sits; a public peer is its own client address, and a log line says once that its forwarding headers were ignored. With TRUST_PROXY_HOPS=0 the peer is used instead of no address, so a directly exposed host no longer shares one bucket across all anonymous callers. Proxies on the same host, Docker network or tailnet need no change.
This commit is contained in:
1 parent
ba22932101
commit
d6d0f7e73f
8 files changed
+309
-20
No files matched your search
@@ -23,7 +23,7 @@ import { ConsentRequiredError } from "./ai/consent-guard";
|
||||
import { attachRecordContextEcho } from "./sharing/record-session-fence";
|
||||
import { refuseOnKeyMismatch } from "./boot/key-mismatch-refusal";
|
||||
import { acceptIncomingRequestId } from "./http/request-id";
|
||||
import { withErrorEnvelopeDefaults } from "./api-response";
|
||||
import { getClientIp, withErrorEnvelopeDefaults } from "./api-response";
|
||||
import {
|
||||
capturedRateLimit,
|
||||
rateLimitResponseHeaders,
|
||||
@@ -190,14 +190,9 @@ export function apiHandler<T extends (...args: any[]) => Promise<Response>>(
|
||||
user_agent:
|
||||
safeRequestProp(request, (r) => r.headers.get("user-agent"), null) ??
|
||||
undefined,
|
||||
ip:
|
||||
safeRequestProp(
|
||||
request,
|
||||
(r) => r.headers.get("x-forwarded-for")?.split(",")[0]?.trim(),
|
||||
null,
|
||||
) ||
|
||||
safeRequestProp(request, (r) => r.headers.get("x-real-ip"), null) ||
|
||||
undefined,
|
||||
// The resolved client address, not the raw leftmost forwarding entry
|
||||
// a direct caller could have written itself.
|
||||
ip: safeRequestProp(request, (r) => getClientIp(r), null) ?? undefined,
|
||||
});
|
||||
|
||||
return runWithRateLimitCapture(() =>
|
||||
|
||||
Reference in new issue
Block a user