open
https://gitlab.synchro.net/main/sbbs/-/work_items/1283
The web server honors `RemoteIPHeader` (e.g. `CF-Connecting-IP`, `X-Forwarded-For`) on **every** connection, whatever the connecting peer is. A client that connects directly, bypassing the proxy, can send that header and be treated as any IP address it names.
Related: #280 asked for a configurable trusted reverse proxy, from the logging angle. This issue covers the same missing check, plus the filtering it affects.
## What a direct client can spoof today
When `RemoteIPHeader` is set, `parse_headers()` copies the header value into `session->host_ip`. These all follow it:
- **Request rate limiter**: the rate-limit key, the `host_exempt` check, and the address `rate_limit_filter()` writes into `ip.can` all use `host_ip`. A client can:
- rotate fake addresses and never get rate-limited,
- claim an exempt address and skip the limiter entirely,
- get an innocent third party's address written into `ip.can`.
- **Access log**: since !735 (d217da6f41, thorough-11-plan, 2026-10-15), the header value also replaces the access-log host. Before that, the log showed the socket peer's address. Tools that parse the access log (fail2ban-style banning, dashboards) can now be fed a forged address.
- **Scripts and CGI**: `http_request.remote_ip` and `REMOTE_ADDR`.
Not affected: the connect-time `ip.can`/`host.can` checks and the login-attempt tracking (`loginBanned()`/`loginSuccess()`), which use the socket address.
!735 already narrowed the input: values that are not a single valid IPv4/IPv6 address are now rejected. Before that, arbitrary text reached `host_ip` and from there the rate-limit keys and `ip.can` entries.
## Proposed fix
Add a `[Web]` setting, e.g. `RemoteIPHeaderTrust`, listing proxy addresses/CIDRs (IPv4 and IPv6), for example Cloudflare's published ranges or a local reverse proxy's address.
- Honor `RemoteIPHeader` only when the socket peer matches the list. Otherwise ignore the header (log it at debug/notice) and keep the socket address.
- With `RemoteIPHeader` set and the trust list empty, either refuse to honor the header or log a startup warning that every client is trusted.
- Optionally allow the list to come from a file (e.g. `ctrl/` or `text/` like `ip.can`) so large provider range lists can be maintained without editing `sbbs.ini`.
### Also worth fixing alongside
- For `X-Forwarded-For` (a comma-separated chain), take the right-most address that is not a trusted proxy, instead of rejecting the whole list as !735 does now.
- Reset `host_ip` to the socket address at the start of each request on a keep-alive connection. Currently a later request that omits the header keeps the earlier request's value (noted in !735 as pre-existing).
- `%a` in `CustomLogFormat` still logs the proxy (noted in !735).
-- *Authored by Claude (Claude Code), on behalf of @rswindell*
--- SBBSecho 3.38-Linux
* Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)