Honour forwarded headers only from trusted proxies (closes #259)
check / check (push) Failing after 8s

Before, the server took the client's address from X-Forwarded-For or X-Real-IP whoever sent them, so a direct client could get past the login rate limit and choose the address and hostname recorded for its session.

A new ClientIP middleware, run before request logging, resolves the address once: forwarded headers count only when the peer is in TRUSTED_PROXIES, which defaults to the RFC 1918 ranges. A set list replaces the default, the empty string trusts nothing, and an invalid entry stops startup. The request log, login rate limiter and session creation all use the resolved address.

A proxy on the same host as a server outside Docker connects from 127.0.0.1 or ::1, which the default does not trust; the README says to list it.

Model: opus-5-5
This commit was merged in pull request #264.
This commit is contained in:
clawbot committed 2026-11-10 22:41:50 +01:00
1 parent bc8aa6bb96
commit a3d9bbfd81
10 files changed
+670 -46

No files matched your search

+61 -20
View File
@@ -1339,8 +1339,9 @@ Every session has a hostmask in IRC's form, `nick!username@hostname`:
from, or that IP address when the lookup gives no name. The lookup waits up to
three seconds.
Over the HTTP API, the server takes the client's IP address from the first entry
of the `X-Forwarded-For` header, then `X-Real-IP`, then the TCP connection (see
Over the HTTP API, the client's IP address is the address of the TCP connection.
When that connection comes from a trusted proxy, the server reads the address
from `X-Forwarded-For` or `X-Real-IP` instead (see
[Reverse proxy](#reverse-proxy)).
Every user can see a hostmask: in `RPL_WHOISUSER` (`311`), `RPL_WHOREPLY`
@@ -1450,10 +1451,9 @@ over the limit gets `429` `too many login attempts, try again later` with
attempt is more than 15 minutes old. Session creation has no rate limit; the
hashcash stamp is its cost.
The IP address comes from the first entry of `X-Forwarded-For`, else from
`X-Real-IP`, else from the TCP connection. Without a reverse proxy that
overwrites those headers, a client can set them itself and so pick any address
(see [Reverse proxy](#reverse-proxy)).
The IP address is the client's address as described in
[Reverse proxy](#reverse-proxy). A client outside the trusted proxies cannot
change it with `X-Forwarded-For` or `X-Real-IP`.
### Writing a client
@@ -1714,10 +1714,14 @@ Every setting is an environment variable. The server also reads a `.env` file in
its working directory, and a YAML file `neoirc.yaml` in `/etc/neoirc/` or
`$HOME/.config/neoirc/` with the same names as keys. A variable from the
environment wins over one in `.env`, and both win over `neoirc.yaml`. An
environment variable set to the empty string counts as unset, except
`IRC_LISTEN_ADDR`, where the empty string turns the IRC listener off. A
`neoirc.yaml` that is not valid YAML stops the server at startup. A setting that
does not parse or is out of range also stops the server at startup, and the
environment variable set to the empty string counts as unset, with two
exceptions:
- For `IRC_LISTEN_ADDR`, the empty string turns the IRC listener off.
- For `TRUSTED_PROXIES`, the empty string trusts no proxy.
A `neoirc.yaml` that is not valid YAML stops the server at startup. A setting
that does not parse or is out of range also stops the server at startup, and the
error names the setting.
- `PORT` (default `8080`): the HTTP port. `0` lets the system pick a free port.
@@ -1747,6 +1751,11 @@ error names the setting.
address. A fraction such as `0.5` is allowed. It must be above `0`.
- `LOGIN_RATE_BURST` (default `5`): how many attempts an address may make at
once before the rate applies. It must be at least `1`.
- `TRUSTED_PROXIES` (default `10.0.0.0/8,172.16.0.0/12,192.168.0.0/16`): the
reverse proxies whose `X-Forwarded-For` and `X-Real-IP` headers the server
believes. It is a comma-separated list of networks, such as `10.0.0.0/8`, and
single addresses, such as `192.0.2.1`. A list replaces the default entirely.
The empty string trusts nothing. See [Reverse proxy](#reverse-proxy).
- `SESSION_IDLE_TIMEOUT` (default `720h`, 30 days): a duration with a unit, `h`
for hours, `m` for minutes or `s` for seconds, such as `720h` or `36h30m`. A
client with no request for this long is removed, and a session left with no
@@ -1804,14 +1813,34 @@ Run the server behind a reverse proxy that terminates TLS. The server itself
speaks plain HTTP, and the auth cookie is `Secure`, so browsers send it only
over HTTPS. The proxy must:
- Set `X-Forwarded-For` to the client's address, replacing any value the client
sent. The server reads that header first and trusts it from anyone; it reads
`X-Real-IP` only when `X-Forwarded-For` is absent. Without a proxy that
overwrites the header, a client can choose its own address, which sets its
hostname and lets it dodge the login rate limit.
- Connect from an address in `TRUSTED_PROXIES`. The default trusts the private
ranges `10.0.0.0/8`, `172.16.0.0/12` and `192.168.0.0/16`. The setting must
hold the address the server sees the proxy connect from. For a proxy on the
same host:
- With the server outside Docker, that address is `127.0.0.1` or `::1`. The
default does not trust them, so set `TRUSTED_PROXIES=127.0.0.1,::1`.
- Through a published Docker port, that address is the Docker bridge
gateway, such as `172.17.0.1`. The default trusts it.
- Add the client's address to `X-Forwarded-For`.
- Wait at least 60 seconds for a response. A long poll can take 30 seconds, and
the server's own write timeout is 60 seconds.
The server finds the client's address in these steps:
- A connection from outside `TRUSTED_PROXIES` is the client. The server ignores
its `X-Forwarded-For` and `X-Real-IP` headers.
- For a connection from a trusted proxy, the server reads `X-Forwarded-For` from
right to left. It skips trusted addresses and takes the first untrusted one.
When every entry is trusted, it takes the leftmost.
- Without `X-Forwarded-For`, the server takes `X-Real-IP`.
- When an entry it reads is not an IP address, the server uses the proxy's
address.
The client's address sets the session's hostname and counts toward the login
rate limit. Any connection from a trusted address can choose its client address
through `X-Forwarded-For`. If clients connect directly from private addresses,
set `TRUSTED_PROXIES` to the proxy's address alone.
Caddy:
```
@@ -1861,9 +1890,21 @@ The IRC listener has no TLS.
The server logs with Go's `slog`, one line per event, to standard output. The
lines are JSON, or plain text when standard output is a terminal. Every HTTP
request is logged with its method, URL, protocol, status, latency, remote
address, user agent, referrer and request ID. `DEBUG=true` adds debug-level
lines.
request is logged with:
- start time
- method
- URL
- protocol
- status
- latency
- client address
- user agent
- referrer
- request ID
The client address is the one found as described in
[Reverse proxy](#reverse-proxy). `DEBUG=true` adds debug-level lines.
### The database
@@ -1946,8 +1987,8 @@ and their sessions to end. Then it closes the database.
gets a message.
- `internal/hashcash/`: stamp checking and minting.
- `internal/ratelimit/`: the per-address login rate limiter.
- `internal/middleware/`: request logging, cross-origin requests, the
`Content-Security-Policy` header and metrics.
- `internal/middleware/`: finding the client's address, request logging,
cross-origin requests, the `Content-Security-Policy` header and metrics.
- `internal/healthcheck/` and `internal/stats/`: the health check and its
counters.
- `internal/logger/` and `internal/globals/`: logging setup and build