diff --git a/axes/decorators.py b/axes/decorators.py index 42cf489..036fe08 100644 --- a/axes/decorators.py +++ b/axes/decorators.py @@ -97,20 +97,32 @@ def is_valid_ip(ip_address): return valid +def is_valid_public_ip(ip_address): + """Returns whether IP address is both valid AND, per RFC 1918, not reserved as + private""" + if not is_valid_ip(ip_address): + return False + PRIVATE_IPS_PREFIX = ( + '10.', + '172.16.', '172.17.', '172.18.', '172.19.', '172.20.', '172.21.', '172.22.', + '172.23.', '172.24.', '172.25.', '172.26.', '172.27.', '172.28.', '172.29.', + '172.30.', '172.31.', + '192.168.', + '127.', + ) + return not ip_address.startswith(PRIVATE_IPS_PREFIX) + def get_ip_address_from_request(request): """ Makes the best attempt to get the client's real IP or return the loopback """ - PRIVATE_IPS_PREFIX = ('10.', '172.', '192.', '127.') ip_address = '' x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '') if x_forwarded_for and ',' not in x_forwarded_for: - if not x_forwarded_for.startswith(PRIVATE_IPS_PREFIX) and is_valid_ip(x_forwarded_for): + if is_valid_public_ip(x_forwarded_for): ip_address = x_forwarded_for.strip() else: for ip_raw in x_forwarded_for.split(','): ip = ip_raw.strip() - if ip.startswith(PRIVATE_IPS_PREFIX): - continue - elif not is_valid_ip(ip): + if not is_valid_public_ip(ip): continue else: ip_address = ip @@ -118,14 +130,14 @@ def get_ip_address_from_request(request): if not ip_address: x_real_ip = request.META.get('HTTP_X_REAL_IP', '') if x_real_ip: - if not x_real_ip.startswith(PRIVATE_IPS_PREFIX) and is_valid_ip(x_real_ip): + if is_valid_public_ip(x_real_ip): ip_address = x_real_ip.strip() if not ip_address: remote_addr = request.META.get('REMOTE_ADDR', '') if remote_addr: - if not remote_addr.startswith(PRIVATE_IPS_PREFIX) and is_valid_ip(remote_addr): + if is_valid_public_ip(remote_addr): ip_address = remote_addr.strip() - if remote_addr.startswith(PRIVATE_IPS_PREFIX) and is_valid_ip(remote_addr): + if not is_valid_public_ip(remote_addr) and is_valid_ip(remote_addr): ip_address = remote_addr.strip() if not ip_address: ip_address = '127.0.0.1' diff --git a/axes/tests.py b/axes/tests.py index db11a97..b847bd5 100644 --- a/axes/tests.py +++ b/axes/tests.py @@ -7,9 +7,11 @@ from django.test.utils import override_settings from django.contrib.auth.models import User from django.core.urlresolvers import NoReverseMatch from django.core.urlresolvers import reverse +from django.utils import six from axes.decorators import COOLOFF_TIME from axes.decorators import FAILURE_LIMIT +from axes.decorators import is_valid_public_ip from axes.models import AccessAttempt, AccessLog from axes.signals import user_locked_out from axes.utils import reset @@ -214,3 +216,26 @@ class AccessAttemptTest(TestCase): extra_data = {string.ascii_letters * x: x for x in range(0, 1000)} # An impossibly large post dict self._login(**extra_data) self.assertEquals(len(AccessAttempt.objects.latest('id').post_data), 1024) + + +class IPClassifierTest(TestCase): + + def test_classify_private_ips(self): + """Tests whether is_valid_public_ip correctly classifies IPs as being + bot public and valid + """ + EXPECTED = { + 'foobar': False, # invalid - not octects + '192.168.0': False, # invalid - only 3 octets + '192.168.0.0': False, # private + '192.168.165.1': False, # private + '192.249.19.1': True, # public but 192 prefix + '10.0.201.13': False, # private + '172.15.12.1': True, # public but 172 prefix + '172.16.12.1': False, # private + '172.31.12.1': False, # private + '172.32.0.1': True, # public but 127 prefix + '200.150.23.5': True, # normal public + } + for ip_address, is_valid_public in six.iteritems(EXPECTED): + self.assertEqual(is_valid_public_ip(ip_address), is_valid_public)