Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 932346f

Browse files
committed
Issue #18805: better netmask validation in ipaddress
1 parent 578c677 commit 932346f

3 files changed

Lines changed: 139 additions & 85 deletions

File tree

Lib/ipaddress.py

Lines changed: 83 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -456,8 +456,8 @@ def _check_packed_address(self, address, expected_len):
456456
raise AddressValueError(msg % (address, address_len,
457457
expected_len, self._version))
458458

459-
def _ip_int_from_prefix(self, prefixlen=None):
460-
"""Turn the prefix length netmask into a int for comparison.
459+
def _ip_int_from_prefix(self, prefixlen):
460+
"""Turn the prefix length into a bitwise netmask
461461
462462
Args:
463463
prefixlen: An integer, the prefix length.
@@ -466,36 +466,92 @@ def _ip_int_from_prefix(self, prefixlen=None):
466466
An integer.
467467
468468
"""
469-
if prefixlen is None:
470-
prefixlen = self._prefixlen
471469
return self._ALL_ONES ^ (self._ALL_ONES >> prefixlen)
472470

473-
def _prefix_from_ip_int(self, ip_int, mask=32):
474-
"""Return prefix length from the decimal netmask.
471+
def _prefix_from_ip_int(self, ip_int):
472+
"""Return prefix length from the bitwise netmask.
475473
476474
Args:
477-
ip_int: An integer, the IP address.
478-
mask: The netmask. Defaults to 32.
475+
ip_int: An integer, the netmask in axpanded bitwise format
476+
477+
Returns:
478+
An integer, the prefix length.
479+
480+
Raises:
481+
ValueError: If the input intermingles zeroes & ones
482+
"""
483+
trailing_zeroes = _count_righthand_zero_bits(ip_int,
484+
self._max_prefixlen)
485+
prefixlen = self._max_prefixlen - trailing_zeroes
486+
leading_ones = ip_int >> trailing_zeroes
487+
all_ones = (1 << prefixlen) - 1
488+
if leading_ones != all_ones:
489+
byteslen = self._max_prefixlen // 8
490+
details = ip_int.to_bytes(byteslen, 'big')
491+
msg = 'Netmask pattern %r mixes zeroes & ones'
492+
raise ValueError(msg % details)
493+
return prefixlen
494+
495+
def _report_invalid_netmask(self, netmask_str):
496+
msg = '%r is not a valid netmask' % netmask_str
497+
raise NetmaskValueError(msg) from None
498+
499+
def _prefix_from_prefix_string(self, prefixlen_str):
500+
"""Return prefix length from a numeric string
501+
502+
Args:
503+
prefixlen_str: The string to be converted
479504
480505
Returns:
481506
An integer, the prefix length.
482507
508+
Raises:
509+
NetmaskValueError: If the input is not a valid netmask
483510
"""
484-
return mask - _count_righthand_zero_bits(ip_int, mask)
511+
# int allows a leading +/- as well as surrounding whitespace,
512+
# so we ensure that isn't the case
513+
if not _BaseV4._DECIMAL_DIGITS.issuperset(prefixlen_str):
514+
self._report_invalid_netmask(prefixlen_str)
515+
try:
516+
prefixlen = int(prefixlen_str)
517+
except ValueError:
518+
self._report_invalid_netmask(prefixlen_str)
519+
if not (0 <= prefixlen <= self._max_prefixlen):
520+
self._report_invalid_netmask(prefixlen_str)
521+
return prefixlen
485522

486-
def _ip_string_from_prefix(self, prefixlen=None):
487-
"""Turn a prefix length into a dotted decimal string.
523+
def _prefix_from_ip_string(self, ip_str):
524+
"""Turn a netmask/hostmask string into a prefix length
488525
489526
Args:
490-
prefixlen: An integer, the netmask prefix length.
527+
ip_str: The netmask/hostmask to be converted
491528
492529
Returns:
493-
A string, the dotted decimal netmask string.
530+
An integer, the prefix length.
494531
532+
Raises:
533+
NetmaskValueError: If the input is not a valid netmask/hostmask
495534
"""
496-
if not prefixlen:
497-
prefixlen = self._prefixlen
498-
return self._string_from_ip_int(self._ip_int_from_prefix(prefixlen))
535+
# Parse the netmask/hostmask like an IP address.
536+
try:
537+
ip_int = self._ip_int_from_string(ip_str)
538+
except AddressValueError:
539+
self._report_invalid_netmask(ip_str)
540+
541+
# Try matching a netmask (this would be /1*0*/ as a bitwise regexp).
542+
# Note that the two ambiguous cases (all-ones and all-zeroes) are
543+
# treated as netmasks.
544+
try:
545+
return self._prefix_from_ip_int(ip_int)
546+
except ValueError:
547+
pass
548+
549+
# Invert the bits, and try matching a /0+1+/ hostmask instead.
550+
ip_int ^= self._ALL_ONES
551+
try:
552+
return self._prefix_from_ip_int(ip_int)
553+
except ValueError:
554+
self._report_invalid_netmask(ip_str)
499555

500556

501557
class _BaseAddress(_IPAddressBase):
@@ -504,7 +560,6 @@ class _BaseAddress(_IPAddressBase):
504560
505561
This IP class contains the version independent methods which are
506562
used by single IP addresses.
507-
508563
"""
509564

510565
def __init__(self, address):
@@ -873,7 +928,7 @@ def subnets(self, prefixlen_diff=1, new_prefix=None):
873928
raise ValueError('prefix length diff must be > 0')
874929
new_prefixlen = self._prefixlen + prefixlen_diff
875930

876-
if not self._is_valid_netmask(str(new_prefixlen)):
931+
if new_prefixlen > self._max_prefixlen:
877932
raise ValueError(
878933
'prefix length diff %d is invalid for netblock %s' % (
879934
new_prefixlen, self))
@@ -1428,33 +1483,16 @@ def __init__(self, address, strict=True):
14281483
self.network_address = IPv4Address(self._ip_int_from_string(addr[0]))
14291484

14301485
if len(addr) == 2:
1431-
mask = addr[1].split('.')
1432-
1433-
if len(mask) == 4:
1434-
# We have dotted decimal netmask.
1435-
if self._is_valid_netmask(addr[1]):
1436-
self.netmask = IPv4Address(self._ip_int_from_string(
1437-
addr[1]))
1438-
elif self._is_hostmask(addr[1]):
1439-
self.netmask = IPv4Address(
1440-
self._ip_int_from_string(addr[1]) ^ self._ALL_ONES)
1441-
else:
1442-
raise NetmaskValueError('%r is not a valid netmask'
1443-
% addr[1])
1444-
1445-
self._prefixlen = self._prefix_from_ip_int(int(self.netmask))
1446-
else:
1447-
# We have a netmask in prefix length form.
1448-
if not self._is_valid_netmask(addr[1]):
1449-
raise NetmaskValueError('%r is not a valid netmask'
1450-
% addr[1])
1451-
self._prefixlen = int(addr[1])
1452-
self.netmask = IPv4Address(self._ip_int_from_prefix(
1453-
self._prefixlen))
1486+
try:
1487+
# Check for a netmask in prefix length form
1488+
self._prefixlen = self._prefix_from_prefix_string(addr[1])
1489+
except NetmaskValueError:
1490+
# Check for a netmask or hostmask in dotted-quad form.
1491+
# This may raise NetmaskValueError.
1492+
self._prefixlen = self._prefix_from_ip_string(addr[1])
14541493
else:
14551494
self._prefixlen = self._max_prefixlen
1456-
self.netmask = IPv4Address(self._ip_int_from_prefix(
1457-
self._prefixlen))
1495+
self.netmask = IPv4Address(self._ip_int_from_prefix(self._prefixlen))
14581496

14591497
if strict:
14601498
if (IPv4Address(int(self.network_address) & int(self.netmask)) !=
@@ -2042,11 +2080,8 @@ def __init__(self, address, strict=True):
20422080
self.network_address = IPv6Address(self._ip_int_from_string(addr[0]))
20432081

20442082
if len(addr) == 2:
2045-
if self._is_valid_netmask(addr[1]):
2046-
self._prefixlen = int(addr[1])
2047-
else:
2048-
raise NetmaskValueError('%r is not a valid netmask'
2049-
% addr[1])
2083+
# This may raise NetmaskValueError
2084+
self._prefixlen = self._prefix_from_prefix_string(addr[1])
20502085
else:
20512086
self._prefixlen = self._max_prefixlen
20522087

@@ -2061,23 +2096,6 @@ def __init__(self, address, strict=True):
20612096
if self._prefixlen == (self._max_prefixlen - 1):
20622097
self.hosts = self.__iter__
20632098

2064-
def _is_valid_netmask(self, prefixlen):
2065-
"""Verify that the netmask/prefixlen is valid.
2066-
2067-
Args:
2068-
prefixlen: A string, the netmask in prefix length format.
2069-
2070-
Returns:
2071-
A boolean, True if the prefix represents a valid IPv6
2072-
netmask.
2073-
2074-
"""
2075-
try:
2076-
prefixlen = int(prefixlen)
2077-
except ValueError:
2078-
return False
2079-
return 0 <= prefixlen <= self._max_prefixlen
2080-
20812099
@property
20822100
def is_site_local(self):
20832101
"""Test if the address is reserved for site-local.

Lib/test/test_ipaddress.py

Lines changed: 53 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -398,18 +398,47 @@ def assertBadAddress(addr, details):
398398
assertBadAddress("::1.2.3.4", "Only decimal digits")
399399
assertBadAddress("1.2.3.256", re.escape("256 (> 255)"))
400400

401+
def test_valid_netmask(self):
402+
self.assertEqual(str(self.factory('192.0.2.0/255.255.255.0')),
403+
'192.0.2.0/24')
404+
for i in range(0, 33):
405+
# Generate and re-parse the CIDR format (trivial).
406+
net_str = '0.0.0.0/%d' % i
407+
net = self.factory(net_str)
408+
self.assertEqual(str(net), net_str)
409+
# Generate and re-parse the expanded netmask.
410+
self.assertEqual(
411+
str(self.factory('0.0.0.0/%s' % net.netmask)), net_str)
412+
# Zero prefix is treated as decimal.
413+
self.assertEqual(str(self.factory('0.0.0.0/0%d' % i)), net_str)
414+
# Generate and re-parse the expanded hostmask. The ambiguous
415+
# cases (/0 and /32) are treated as netmasks.
416+
if i in (32, 0):
417+
net_str = '0.0.0.0/%d' % (32 - i)
418+
self.assertEqual(
419+
str(self.factory('0.0.0.0/%s' % net.hostmask)), net_str)
420+
401421
def test_netmask_errors(self):
402422
def assertBadNetmask(addr, netmask):
403-
msg = "%r is not a valid netmask"
404-
with self.assertNetmaskError(msg % netmask):
423+
msg = "%r is not a valid netmask" % netmask
424+
with self.assertNetmaskError(re.escape(msg)):
405425
self.factory("%s/%s" % (addr, netmask))
406426

407427
assertBadNetmask("1.2.3.4", "")
428+
assertBadNetmask("1.2.3.4", "-1")
429+
assertBadNetmask("1.2.3.4", "+1")
430+
assertBadNetmask("1.2.3.4", " 1 ")
431+
assertBadNetmask("1.2.3.4", "0x1")
408432
assertBadNetmask("1.2.3.4", "33")
409433
assertBadNetmask("1.2.3.4", "254.254.255.256")
434+
assertBadNetmask("1.2.3.4", "1.a.2.3")
410435
assertBadNetmask("1.1.1.1", "254.xyz.2.3")
411436
assertBadNetmask("1.1.1.1", "240.255.0.0")
437+
assertBadNetmask("1.1.1.1", "255.254.128.0")
438+
assertBadNetmask("1.1.1.1", "0.1.127.255")
412439
assertBadNetmask("1.1.1.1", "pudding")
440+
assertBadNetmask("1.1.1.1", "::")
441+
413442

414443
class InterfaceTestCase_v4(BaseTestCase, NetmaskTestMixin_v4):
415444
factory = ipaddress.IPv4Interface
@@ -438,17 +467,34 @@ def assertBadAddress(addr, details):
438467
assertBadAddress("10/8", "At least 3 parts")
439468
assertBadAddress("1234:axy::b", "Only hex digits")
440469

470+
def test_valid_netmask(self):
471+
# We only support CIDR for IPv6, because expanded netmasks are not
472+
# standard notation.
473+
self.assertEqual(str(self.factory('2001:db8::/32')), '2001:db8::/32')
474+
for i in range(0, 129):
475+
# Generate and re-parse the CIDR format (trivial).
476+
net_str = '::/%d' % i
477+
self.assertEqual(str(self.factory(net_str)), net_str)
478+
# Zero prefix is treated as decimal.
479+
self.assertEqual(str(self.factory('::/0%d' % i)), net_str)
480+
441481
def test_netmask_errors(self):
442482
def assertBadNetmask(addr, netmask):
443-
msg = "%r is not a valid netmask"
444-
with self.assertNetmaskError(msg % netmask):
483+
msg = "%r is not a valid netmask" % netmask
484+
with self.assertNetmaskError(re.escape(msg)):
445485
self.factory("%s/%s" % (addr, netmask))
446486

447487
assertBadNetmask("::1", "")
448488
assertBadNetmask("::1", "::1")
449489
assertBadNetmask("::1", "1::")
490+
assertBadNetmask("::1", "-1")
491+
assertBadNetmask("::1", "+1")
492+
assertBadNetmask("::1", " 1 ")
493+
assertBadNetmask("::1", "0x1")
450494
assertBadNetmask("::1", "129")
495+
assertBadNetmask("::1", "1.2.3.4")
451496
assertBadNetmask("::1", "pudding")
497+
assertBadNetmask("::", "::")
452498

453499
class InterfaceTestCase_v6(BaseTestCase, NetmaskTestMixin_v6):
454500
factory = ipaddress.IPv6Interface
@@ -694,16 +740,14 @@ def testGetNetmask(self):
694740
def testZeroNetmask(self):
695741
ipv4_zero_netmask = ipaddress.IPv4Interface('1.2.3.4/0')
696742
self.assertEqual(int(ipv4_zero_netmask.network.netmask), 0)
697-
self.assertTrue(ipv4_zero_netmask.network._is_valid_netmask(
698-
str(0)))
743+
self.assertEqual(ipv4_zero_netmask._prefix_from_prefix_string('0'), 0)
699744
self.assertTrue(ipv4_zero_netmask._is_valid_netmask('0'))
700745
self.assertTrue(ipv4_zero_netmask._is_valid_netmask('0.0.0.0'))
701746
self.assertFalse(ipv4_zero_netmask._is_valid_netmask('invalid'))
702747

703748
ipv6_zero_netmask = ipaddress.IPv6Interface('::1/0')
704749
self.assertEqual(int(ipv6_zero_netmask.network.netmask), 0)
705-
self.assertTrue(ipv6_zero_netmask.network._is_valid_netmask(
706-
str(0)))
750+
self.assertEqual(ipv6_zero_netmask._prefix_from_prefix_string('0'), 0)
707751

708752
def testIPv4NetAndHostmasks(self):
709753
net = self.ipv4_network
@@ -719,7 +763,7 @@ def testIPv4NetAndHostmasks(self):
719763
self.assertFalse(net._is_hostmask('1.2.3.4'))
720764

721765
net = ipaddress.IPv4Network('127.0.0.0/0.0.0.255')
722-
self.assertEqual(24, net.prefixlen)
766+
self.assertEqual(net.prefixlen, 24)
723767

724768
def testGetBroadcast(self):
725769
self.assertEqual(int(self.ipv4_network.broadcast_address), 16909311)
@@ -1271,11 +1315,6 @@ def testPacked(self):
12711315
self.assertEqual(ipaddress.IPv6Interface('::1:0:0:0:0').packed,
12721316
b'\x00' * 6 + b'\x00\x01' + b'\x00' * 8)
12731317

1274-
def testIpStrFromPrefixlen(self):
1275-
ipv4 = ipaddress.IPv4Interface('1.2.3.4/24')
1276-
self.assertEqual(ipv4._ip_string_from_prefix(), '255.255.255.0')
1277-
self.assertEqual(ipv4._ip_string_from_prefix(28), '255.255.255.240')
1278-
12791318
def testIpType(self):
12801319
ipv4net = ipaddress.ip_network('1.2.3.4')
12811320
ipv4addr = ipaddress.ip_address('1.2.3.4')
@@ -1467,14 +1506,8 @@ def testHash(self):
14671506
def testIPBases(self):
14681507
net = self.ipv4_network
14691508
self.assertEqual('1.2.3.0/24', net.compressed)
1470-
self.assertEqual(
1471-
net._ip_int_from_prefix(24),
1472-
net._ip_int_from_prefix(None))
14731509
net = self.ipv6_network
14741510
self.assertRaises(ValueError, net._string_from_ip_int, 2**128 + 1)
1475-
self.assertEqual(
1476-
self.ipv6_address._string_from_ip_int(self.ipv6_address._ip),
1477-
self.ipv6_address._string_from_ip_int(None))
14781511

14791512
def testIPv6NetworkHelpers(self):
14801513
net = self.ipv6_network

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ Core and Builtins
4848
Library
4949
-------
5050

51+
- Issue #18805: the netmask/hostmask parsing in ipaddress now more reliably
52+
filters out illegal values
53+
5154
- Issue #17369: get_filename was raising an exception if the filename
5255
parameter's RFC2231 encoding was broken in certain ways. This was
5356
a regression relative to python2.

0 commit comments

Comments
 (0)