Closes #22192: Introduce HTTP_CLIENT_IP_HEADERS configuration parameter (#22197)

This commit is contained in:
Jeremy Stretch 2026-05-14 11:04:30 -04:00 committed by GitHub
parent d4408f3d5d
commit 329c041224
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 61 additions and 8 deletions

View File

@ -80,6 +80,26 @@ The hostname displayed in the user interface identifying the system on which Net
---
## HTTP_CLIENT_IP_HEADERS
!!! info "This parameter was introduced in NetBox v4.6.1."
Default:
```python
(
'HTTP_X_REAL_IP',
'HTTP_X_FORWARDED_FOR',
'REMOTE_ADDR',
)
```
An ordered list of HTTP request headers inspected to determine the source IP address of a client request. The first header in the list which is present on the request is used; if none are found, the client IP cannot be determined. This is most commonly required when NetBox is deployed behind a reverse proxy which injects a proprietary client IP header (e.g. `HTTP_CF_CONNECTING_IP` for Cloudflare).
The client IP is used for source-address restrictions on API tokens and for logging failed login attempts.
---
## HTTP_PROXIES
Default: `None`

View File

@ -132,6 +132,11 @@ GRAPHQL_DEFAULT_VERSION = getattr(configuration, 'GRAPHQL_DEFAULT_VERSION', 1)
GRAPHQL_MAX_ALIASES = getattr(configuration, 'GRAPHQL_MAX_ALIASES', 10)
GRAPHQL_MAX_QUERY_DEPTH = getattr(configuration, 'GRAPHQL_MAX_QUERY_DEPTH', None)
HOSTNAME = getattr(configuration, 'HOSTNAME', platform.node())
HTTP_CLIENT_IP_HEADERS = getattr(configuration, 'HTTP_CLIENT_IP_HEADERS', (
'HTTP_X_REAL_IP',
'HTTP_X_FORWARDED_FOR',
'REMOTE_ADDR',
))
HTTP_PROXIES = getattr(configuration, 'HTTP_PROXIES', {})
INTERNAL_IPS = getattr(configuration, 'INTERNAL_IPS', ('127.0.0.1', '::1'))
ISOLATED_DEPLOYMENT = getattr(configuration, 'ISOLATED_DEPLOYMENT', False)

View File

@ -2,6 +2,7 @@ import warnings
from contextlib import ExitStack, contextmanager
from urllib.parse import urlparse
from django.conf import settings
from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.translation import gettext_lazy as _
from netaddr import AddrFormatError, IPAddress
@ -71,15 +72,14 @@ def copy_safe_request(request, include_files=True):
def get_client_ip(request, additional_headers=()):
"""
Return the client (source) IP address of the given request.
Return the client (source) IP address of the given request. Accepts an optional list of headers to inspect in
addition to those configured under HTTP_CLIENT_IP_HEADERS.
"""
HTTP_HEADERS = (
'HTTP_X_REAL_IP',
'HTTP_X_FORWARDED_FOR',
'REMOTE_ADDR',
*additional_headers
headers = (
*settings.HTTP_CLIENT_IP_HEADERS,
*additional_headers,
)
for header in HTTP_HEADERS:
for header in headers:
if header in request.META:
ip = request.META[header].split(',')[0].strip()
try:

View File

@ -1,5 +1,5 @@
from django.contrib.auth.models import AnonymousUser
from django.test import RequestFactory, TestCase
from django.test import RequestFactory, TestCase, override_settings
from netaddr import IPAddress
from utilities.request import copy_safe_request, get_client_ip
@ -61,3 +61,31 @@ class GetClientIPTestCase(TestCase):
request = self.factory.get('/', HTTP_X_FORWARDED_FOR='invalid_ip')
with self.assertRaises(ValueError):
get_client_ip(request)
def test_no_matching_header(self):
request = self.factory.get('/')
request.META.pop('REMOTE_ADDR', None)
self.assertIsNone(get_client_ip(request))
def test_additional_headers_argument(self):
"""Headers passed via `additional_headers` are checked after the configured defaults."""
request = self.factory.get('/', HTTP_CF_CONNECTING_IP='10.0.0.1')
request.META.pop('REMOTE_ADDR', None)
self.assertEqual(
get_client_ip(request, additional_headers=('HTTP_CF_CONNECTING_IP',)),
IPAddress('10.0.0.1'),
)
def test_default_headers_precede_additional_headers(self):
"""Headers from HTTP_CLIENT_IP_HEADERS take precedence over `additional_headers`."""
request = self.factory.get('/', HTTP_X_FORWARDED_FOR='192.168.1.1', HTTP_CF_CONNECTING_IP='10.0.0.1')
self.assertEqual(
get_client_ip(request, additional_headers=('HTTP_CF_CONNECTING_IP',)),
IPAddress('192.168.1.1'),
)
@override_settings(HTTP_CLIENT_IP_HEADERS=('HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR'))
def test_custom_configured_headers(self):
"""get_client_ip() should honor the HTTP_CLIENT_IP_HEADERS setting."""
request = self.factory.get('/', HTTP_X_FORWARDED_FOR='192.168.1.1', HTTP_CF_CONNECTING_IP='10.0.0.1')
self.assertEqual(get_client_ip(request), IPAddress('10.0.0.1'))