v1.16.0 — Trusted-proxy client IP resolution
Security fix
X-Forwarded-For and X-Real-IP were honoured unconditionally by all three servers. Any caller reaching the server directly could set them to a clean residential IP and skip every IP-derived check — datacenter ranges, Tor/VPN, rate limiting, token IP binding, and PoW difficulty scaling.
These headers are now read only when the socket peer is a configured proxy.
TRUSTED_PROXIES
New environment variable: a comma-separated list of CIDRs and bare IPs.
| Value | Meaning |
|---|---|
| unset | Loopback plus private/link-local ranges (the default) |
10.0.0.0/8,203.0.113.7 |
Only these peers |
* |
Every peer — only when an edge you control always overwrites the headers |
none |
No peer; always use the socket address |
The default covers nginx-on-localhost, sidecar proxies and in-cluster ingresses with no configuration, while a directly exposed server sees public peers and ignores their headers.
Upgrading: if your reverse proxy reaches FCaptcha from outside those ranges — a Cloudflare Tunnel, a load balancer in another VPC, or a PaaS edge (Railway's is in the RFC 6598 CGNAT block, 100.64.0.0/10) — set TRUSTED_PROXIES explicitly. Otherwise every visitor is attributed to the proxy, which quietly ruins rate limiting and IP reputation. The server logs a one-time warning naming the address to add, so check your logs after upgrading.
Also in this release
X-Forwarded-Foris walked right-to-left, skipping trusted hops, instead of taking the leftmost entry. A client can prepend to the chain even behind a legitimate proxy, so the leftmost value was never the client's.X-JA3-HashandTRUSTED_JA4_HEADERSare gated on the same check. That allowlist covered header names, which says nothing about who set them — a client could sendcf-ja4itself and present a clean fingerprint.- Removed chi's
middleware.RealIPfrom the Go server. It rewroter.RemoteAddrfrom the same headers for every caller, leaving no un-forged source to fall back on. - Address normalisation (port stripped, IPv4-mapped IPv6 un-mapped). The Go server was passing
RemoteAddrwith its port toIsDatacenterIPon the no-header path, so that check silently never matched. - Misconfiguration warning, capped at 10 occurrences, naming the peer to add.
Tests
New unit suites in all three languages (clientip_test.go, clientip.test.js, test_clientip.py) covering spoof rejection, chain walking, address normalisation, spec parsing and the warning cap. test/test-detection.js remains at 86/86.