client IP is the last X-Forwarded-For hop, not the first

NPM sets the header with $proxy_add_x_forwarded_for, which appends the
connecting address to whatever the client sent. The first hop is therefore
client-controlled and the last is the one NPM vouches for. Reading the
first hop would have let an outsider claim a LAN address with one header,
and admin_api.client_is_lan decides on it. Found while verifying the nginx
block removal; fixed before the block came out was relied on. Also
tightens login throttling, which used the same helper.
This commit is contained in:
2026-09-04 17:48:15 -04:00
parent 69b177d519
commit c458b8e64d
2 changed files with 15 additions and 2 deletions
+11 -2
View File
@@ -42,10 +42,19 @@ def _render(title, body):
def _client_ip(request):
"""Real client address. The app sits behind NPM, so request.client.host is
the proxy on every hit and would throttle the whole site as one address."""
the proxy on every hit and would throttle the whole site as one address.
The LAST hop, not the first. NPM sets the header with
$proxy_add_x_forwarded_for, which APPENDS the connecting address to
whatever the client sent, so the first hop is client-controlled and the
last is the one NPM vouches for. With exactly one trusted proxy in front
(the app is published on 127.0.0.1 and the docker network only), the
rightmost address is the real client. Reading the first hop would let an
outsider claim a LAN address with one header - and that is exactly what
admin_api.client_is_lan decides on."""
fwd = request.headers.get("x-forwarded-for", "")
if fwd:
return fwd.split(",")[0].strip()
return fwd.split(",")[-1].strip()
return request.client.host if request.client else None