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
+4
View File
@@ -170,6 +170,10 @@ class _Req:
for ip, want in (("10.0.0.55", True), ("10.0.1.20", True), ("127.0.0.1", True), ("172.19.0.31", True),
("108.36.248.87", False), ("192.168.1.9", False), ("", False), ("garbage", False)):
check("client_is_lan(%r) is %s" % (ip, want), A.client_is_lan(_Req(ip)) == want)
check("forged first hop does not make an outsider LAN: last hop wins",
A.client_is_lan(_Req("10.0.0.1, 108.36.248.87")) is False)
check("and a forged outside hop does not make a LAN caller outside",
A.client_is_lan(_Req("203.0.113.9, 10.0.0.55")) is True)
check("api_keys_from defaults to lan", I.get_setting("api_keys_from") == "lan")
raises("api_keys_from rejects other values", 422, I.IdentityError, I.set_setting, "api_keys_from", "vpn")
# a key from outside is refused while lan, allowed once anywhere, admin token never from outside