Honour X-Forwarded-For and X-Real-IP only from trusted proxies #259

Closed
opened 2026-11-10 20:52:23 +01:00 by clawbot · 1 comment
Collaborator

clientIP in internal/handlers/api.go takes the client's address from X-Forwarded-For or X-Real-IP whoever the connecting peer is. A client that connects directly can send a new X-Forwarded-For value with each POST /api/v1/login and never hit the login rate limit, and it picks the IP address and hostname recorded for its session. The request log has the opposite fault: it logs the TCP peer (ipFromHostPort(request.RemoteAddr) in internal/middleware/middleware.go), so behind a reverse proxy every line shows the proxy.

REPO_POLICIES.md ("Reverse proxy awareness") allows forwarded headers only from a configured set of trusted proxies. The standing ruling for every app sets the default: all RFC 1918 addresses.

Definition of done

  • New setting TRUSTED_PROXIES, read in internal/config: a comma-separated list of CIDR prefixes or single addresses.
    • Unset: the trusted set is 10.0.0.0/8, 172.16.0.0/12 and 192.168.0.0/16.
    • Set: the list replaces the default entirely.
    • Set to the empty string: nothing is trusted.
    • Set but not parseable: startup fails with an error naming TRUSTED_PROXIES, like the other settings.
  • A middleware in internal/middleware resolves the client address once per request and replaces request.RemoteAddr with it. It runs before Logging in internal/server/routes.go, so the request log, the login rate limiter and the session IP address and hostname (internal/handlers/auth.go, internal/handlers/api.go) all get the resolved address. clientIP stops reading headers.
    • Peer not trusted: the peer's address; both headers ignored.
    • Peer trusted: walk X-Forwarded-For from the right, skip trusted addresses, take the first untrusted one; when every entry is trusted, the leftmost. Without X-Forwarded-For, X-Real-IP. When an entry that the walk reaches does not parse as an IP address, the peer's address.
    • An IPv4-mapped IPv6 peer (::ffff:10.0.0.1) is unmapped before the check.
  • The trusted set is a go4.org/netipx IPSet (the decided library for IP address sets), not a hand-written list of ranges.
  • The IRC listener (internal/ircserver) is plain TCP with no headers and is left alone.
  • README.md: document TRUSTED_PROXIES under "Configuration", and rewrite the three passages that now say the headers are trusted from anyone: the hostname paragraph (near "Over the HTTP API, the server takes the client's IP address"), "Login rate limit", and "Reverse proxy". State that a peer inside the trusted set chooses its client address through X-Forwarded-For, so an operator whose clients connect directly from private addresses sets the list to the proxy's address alone. "Logging" says the logged address is the resolved client address.
  • Tests, table-driven, in the external test packages:
    • internal/middleware: untrusted peer with forged headers gives the peer; trusted peer with a chain gives the rightmost untrusted entry; an all-trusted chain gives the leftmost; X-Real-IP used only without X-Forwarded-For; an unparseable entry gives the peer; a mapped IPv6 peer.
    • internal/config: unset gives the default, a value replaces it, the empty string trusts nothing, an invalid value fails startup.
    • internal/handlers: a client connecting from an untrusted address cannot get past the login rate limit by changing X-Forwarded-For.

Read first: AGENTS.md, CONVENTIONS.md, REPO_POLICIES.md. The same rule is built in sneak/webhooker (internal/middleware/clientip.go on next); there an empty value counts as unset, here it trusts nothing.

Model: opus-5-5

`clientIP` in `internal/handlers/api.go` takes the client's address from `X-Forwarded-For` or `X-Real-IP` whoever the connecting peer is. A client that connects directly can send a new `X-Forwarded-For` value with each `POST /api/v1/login` and never hit the login rate limit, and it picks the IP address and hostname recorded for its session. The request log has the opposite fault: it logs the TCP peer (`ipFromHostPort(request.RemoteAddr)` in `internal/middleware/middleware.go`), so behind a reverse proxy every line shows the proxy. `REPO_POLICIES.md` ("Reverse proxy awareness") allows forwarded headers only from a configured set of trusted proxies. The standing ruling for every app sets the default: all RFC 1918 addresses. ## Definition of done - New setting `TRUSTED_PROXIES`, read in `internal/config`: a comma-separated list of CIDR prefixes or single addresses. - Unset: the trusted set is `10.0.0.0/8`, `172.16.0.0/12` and `192.168.0.0/16`. - Set: the list replaces the default entirely. - Set to the empty string: nothing is trusted. - Set but not parseable: startup fails with an error naming `TRUSTED_PROXIES`, like the other settings. - A middleware in `internal/middleware` resolves the client address once per request and replaces `request.RemoteAddr` with it. It runs before `Logging` in `internal/server/routes.go`, so the request log, the login rate limiter and the session IP address and hostname (`internal/handlers/auth.go`, `internal/handlers/api.go`) all get the resolved address. `clientIP` stops reading headers. - Peer not trusted: the peer's address; both headers ignored. - Peer trusted: walk `X-Forwarded-For` from the right, skip trusted addresses, take the first untrusted one; when every entry is trusted, the leftmost. Without `X-Forwarded-For`, `X-Real-IP`. When an entry that the walk reaches does not parse as an IP address, the peer's address. - An IPv4-mapped IPv6 peer (`::ffff:10.0.0.1`) is unmapped before the check. - The trusted set is a `go4.org/netipx` `IPSet` (the decided library for IP address sets), not a hand-written list of ranges. - The IRC listener (`internal/ircserver`) is plain TCP with no headers and is left alone. - `README.md`: document `TRUSTED_PROXIES` under "Configuration", and rewrite the three passages that now say the headers are trusted from anyone: the `hostname` paragraph (near "Over the HTTP API, the server takes the client's IP address"), "Login rate limit", and "Reverse proxy". State that a peer inside the trusted set chooses its client address through `X-Forwarded-For`, so an operator whose clients connect directly from private addresses sets the list to the proxy's address alone. "Logging" says the logged address is the resolved client address. - Tests, table-driven, in the external test packages: - `internal/middleware`: untrusted peer with forged headers gives the peer; trusted peer with a chain gives the rightmost untrusted entry; an all-trusted chain gives the leftmost; `X-Real-IP` used only without `X-Forwarded-For`; an unparseable entry gives the peer; a mapped IPv6 peer. - `internal/config`: unset gives the default, a value replaces it, the empty string trusts nothing, an invalid value fails startup. - `internal/handlers`: a client connecting from an untrusted address cannot get past the login rate limit by changing `X-Forwarded-For`. Read first: `AGENTS.md`, `CONVENTIONS.md`, `REPO_POLICIES.md`. The same rule is built in `sneak/webhooker` (`internal/middleware/clientip.go` on `next`); there an empty value counts as unset, here it trusts nothing. Model: opus-5-5
clawbot self-assigned this 2026-11-10 20:52:23 +01:00
Author
Collaborator

Built in #264, branch issue-259-trusted-proxies, as specified in the definition of done.

  • request.RemoteAddr keeps host:port form, with the TCP connection's port, so clientIP and the request log read it unchanged.
  • The README's nginx example proxies from 127.0.0.1, which the default set does not trust. The README tells that operator to set TRUSTED_PROXIES=127.0.0.1.

Model: opus-5-5

Built in https://git.eeqj.de/sneak/neoirc/pulls/264, branch `issue-259-trusted-proxies`, as specified in the definition of done. - `request.RemoteAddr` keeps `host:port` form, with the TCP connection's port, so `clientIP` and the request log read it unchanged. - The README's nginx example proxies from `127.0.0.1`, which the default set does not trust. The README tells that operator to set `TRUSTED_PROXIES=127.0.0.1`. Model: opus-5-5
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/neoirc#259