From 1b10e546110b990becced6d63af851b5548b56d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleksi=20H=C3=A4kli?= Date: Thu, 6 Apr 2017 19:50:52 +0300 Subject: [PATCH 1/2] Fixed #224 -- Add AXES_NUM_PROXIES setting This enables secure calculation of client IP value by allowing the end users to set the number of proxies they have in their current setups --- axes/decorators.py | 48 +++++++++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/axes/decorators.py b/axes/decorators.py index e26df06..f72872a 100644 --- a/axes/decorators.py +++ b/axes/decorators.py @@ -51,15 +51,15 @@ def get_ip(request): if BEHIND_REVERSE_PROXY: # For requests originating from behind a reverse proxy, # resolve the IP address from the given AXES_REVERSE_PROXY_HEADER. - # AXES_REVERSE_PROXY_HEADER defaults to HTTP_X_FORWARDED_FOR if not given, - # which is the Django calling format for the HTTP X-Forwarder-For header. + # AXES_REVERSE_PROXY_HEADER defaults to HTTP_X_FORWARDED_FOR, + # which is the Django name for the HTTP X-Forwarder-For header. # Please see RFC7239 for additional information: # https://tools.ietf.org/html/rfc7239#section-5 # The REVERSE_PROXY_HEADER HTTP header is a list # of potentionally unsecure IPs, for example: # X-Forwarded-For: 1.1.1.1, 11.11.11.11:8080, 111.111.111.111 - ip = request.META.get(REVERSE_PROXY_HEADER, '') + ip_str = request.META.get(REVERSE_PROXY_HEADER, '') # We need to know the number of proxies present in the request chain # in order to securely calculate the one IP that is the real client IP. @@ -68,23 +68,45 @@ def get_ip(request): # configurations, with e.g. the X-Forwarded-For header containing # the originating client IP, proxies and possibly spoofed values. # - # If you are using a special header for client calculation such as - # the X-Real-IP or the like with nginx, please check this configuration. + # If you are using a special header for client calculation such as the + # X-Real-IP or the like with nginx, please check this configuration. # # Please see discussion for more information: # https://github.com/jazzband/django-axes/issues/224 - ip = [ip.strip() for ip in ip.split(',')][-NUM_PROXIES] + ip_list = [ip.strip() for ip in ip_str.split(',')] - # Fix IIS adding client port number to 'X-Forwarded-For' header (strip port) - if not is_ipv6(ip): - ip = ip.split(':', 1)[0] + # Pick the nth last IP in the given list of addresses after parsing + if len(ip_list) >= NUM_PROXIES: + ip = ip_list[-NUM_PROXIES] + + # Fix IIS adding client port number to the + # 'X-Forwarded-For' header (strip port) + if not is_ipv6(ip): + ip = ip.split(':', 1)[0] + + # If nth last is not found, default to no IP and raise a warning + else: + ip = '' + raise Warning( + 'AXES: Axes is configured for operation behind a ' + 'reverse proxy but received too few IPs in the HTTP ' + 'AXES_REVERSE_PROXY_HEADER. Check your ' + 'AXES_NUM_PROXIES configuration. ' + 'Header name: {0}, value: {1}'.format( + REVERSE_PROXY_HEADER, ip_str + ) + ) if not ip: raise Warning( - 'AXES: Axes is configured for operation behind a reverse proxy ' - 'but could not find an HTTP header value. Check your proxy ' - 'server settings to make sure this header value is being ' - 'passed. Header name {0}'.format(REVERSE_PROXY_HEADER) + 'AXES: Axes is configured for operation behind a reverse ' + 'proxy but could not find a suitable IP in the specified ' + 'HTTP header. Check your proxy server settings to make ' + 'sure correct headers are being passed to Django in ' + 'AXES_REVERSE_PROXY_HEADER. ' + 'Header name: {0}, value: {1}'.format( + REVERSE_PROXY_HEADER, ip_str + ) ) return ip From 919df8ebf77e3745f86560394997271aa5121771 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleksi=20H=C3=A4kli?= Date: Thu, 6 Apr 2017 20:03:10 +0300 Subject: [PATCH 2/2] Add tests for proxy number parametrization --- .travis.yml | 1 + axes/test_settings_num_proxies.py | 5 +++++ axes/tests.py | 30 ++++++++++++++++++++++++++++++ runtests_num_proxies.py | 8 ++++++++ 4 files changed, 44 insertions(+) create mode 100644 axes/test_settings_num_proxies.py create mode 100644 runtests_num_proxies.py diff --git a/.travis.yml b/.travis.yml index 5df10d4..f488e0e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,7 @@ install: script: - coverage run -a --source=axes runtests.py - coverage run -a --source=axes runtests_proxy.py +- coverage run -a --source=axes runtests_num_proxies.py - coverage run -a --source=axes runtests_proxy_custom_header.py - coverage report after_success: diff --git a/axes/test_settings_num_proxies.py b/axes/test_settings_num_proxies.py new file mode 100644 index 0000000..53f3166 --- /dev/null +++ b/axes/test_settings_num_proxies.py @@ -0,0 +1,5 @@ +from .test_settings import * + +AXES_BEHIND_REVERSE_PROXY = True +AXES_REVERSE_PROXY_HEADER = 'HTTP_X_FORWARDED_FOR' +AXES_NUM_PROXIES = 2 diff --git a/axes/tests.py b/axes/tests.py index 1c32c6d..4849b2c 100644 --- a/axes/tests.py +++ b/axes/tests.py @@ -538,3 +538,33 @@ class GetIPProxyCustomHeaderTest(TestCase): for header in valid_headers: self.request.META[settings.AXES_REVERSE_PROXY_HEADER] = header self.assertEqual(self.ip, get_ip(self.request)) + +class GetIPNumProxiesTest(TestCase): + """Test that get_ip returns the correct last IP when NUM_PROXIES is configured + """ + + def setUp(self): + self.request = MockRequest() + + def test_header_ordering(self): + self.ip = '2.2.2.2' + + valid_headers = [ + '4.4.4.4, 3.3.3.3, 2.2.2.2, 1.1.1.1', + ' 3.3.3.3, 2.2.2.2, 1.1.1.1', + ' 2.2.2.2, 1.1.1.1', + ] + + for header in valid_headers: + self.request.META[settings.AXES_REVERSE_PROXY_HEADER] = header + self.assertEqual(self.ip, get_ip(self.request)) + + def test_invalid_headers_too_few(self): + self.request.META[settings.AXES_REVERSE_PROXY_HEADER] = '1.1.1.1' + with self.assertRaises(Warning): + get_ip(self.request) + + def test_invalid_headers_no_ip(self): + self.request.META[settings.AXES_REVERSE_PROXY_HEADER] = '' + with self.assertRaises(Warning): + get_ip(self.request) diff --git a/runtests_num_proxies.py b/runtests_num_proxies.py new file mode 100644 index 0000000..a3dbba8 --- /dev/null +++ b/runtests_num_proxies.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python + +from runtests import run_tests + +if __name__ == '__main__': + run_tests('axes.test_settings_num_proxies', [ + 'axes.tests.GetIPNumProxiesTest', + ])