net: strip the ephemeral source port from the forwarded client IP

IIS ARR sets X-Forwarded-For to clientip:port, and the port changes every
connection. Left in, the audit log showed IP:PORT, the dashboard IP fallback
never matched a stored (portless) DashboardDefault.ipaddress, and login rate
limiting keyed per-connection instead of per-host. Add an IPv6-safe
clientip.client_ip / strip_port helper and use it in the audit log, the
dashboard resolver, and the login rate-limit key.
This commit is contained in:
cproudlock
2026-07-29 10:06:18 -04:00
parent 8dce622392
commit ced356882c
5 changed files with 95 additions and 14 deletions

View File

@@ -0,0 +1,45 @@
"""strip_port / client_ip normalization (ARR forwards clientip:port)."""
from shopdb.utils.clientip import strip_port, client_ip
def test_strip_ipv4_port():
assert strip_port('10.134.48.20:52344') == '10.134.48.20'
def test_ipv4_without_port_untouched():
assert strip_port('10.134.48.20') == '10.134.48.20'
def test_bare_ipv6_untouched():
assert strip_port('2001:db8::1') == '2001:db8::1'
def test_bracketed_ipv6_with_port():
assert strip_port('[2001:db8::1]:443') == '2001:db8::1'
def test_bracketed_ipv6_without_port():
assert strip_port('[2001:db8::1]') == '2001:db8::1'
def test_none_and_empty_safe():
assert strip_port(None) is None
assert strip_port('') == ''
class _FakeRequest:
def __init__(self, headers=None, remote_addr=None):
self.headers = headers or {}
self.remote_addr = remote_addr
def test_client_ip_prefers_forwarded_first_hop_stripped():
request_obj = _FakeRequest(headers={'X-Forwarded-For': '10.0.0.5:51000, 10.0.0.1'},
remote_addr='127.0.0.1')
assert client_ip(request_obj) == '10.0.0.5'
def test_client_ip_falls_back_to_remote_addr_stripped():
request_obj = _FakeRequest(headers={}, remote_addr='192.168.1.9:60000')
assert client_ip(request_obj) == '192.168.1.9'