• Web server: honor RemoteIPHeader only from trusted proxy addresses

    From Rob Swindell@1:103/705 to GitLab issue in main/sbbs on Thu Oct 15 14:00:37 2026
    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)