diff --git a/changelogs/fragments/87235-is_netmask-contiguous.yml b/changelogs/fragments/87235-is_netmask-contiguous.yml new file mode 100644 index 00000000000..127e6ea2900 --- /dev/null +++ b/changelogs/fragments/87235-is_netmask-contiguous.yml @@ -0,0 +1,5 @@ +bugfixes: + - module_utils - ``is_netmask`` now rejects non-contiguous netmasks such as + ``255.255.0.255``, where each octet is individually valid but the mask is not + a contiguous run of network bits followed by host bits + (https://github.com/ansible/ansible/pull/87235). diff --git a/lib/ansible/module_utils/common/network.py b/lib/ansible/module_utils/common/network.py index a5c2148c06f..6d82b6b8b09 100644 --- a/lib/ansible/module_utils/common/network.py +++ b/lib/ansible/module_utils/common/network.py @@ -21,13 +21,17 @@ def is_netmask(val): parts = str(val).split('.') if not len(parts) == 4: return False + mask = 0 for part in parts: try: - if int(part) not in VALID_MASKS: - raise ValueError + octet = int(part) except ValueError: return False - return True + if octet not in VALID_MASKS: + return False + mask = (mask << 8) | octet + inverted = mask ^ 0xFFFFFFFF + return inverted & (inverted + 1) == 0 def is_masklen(val): diff --git a/test/units/module_utils/common/test_network.py b/test/units/module_utils/common/test_network.py index 012693b407d..481e96c99fd 100644 --- a/test/units/module_utils/common/test_network.py +++ b/test/units/module_utils/common/test_network.py @@ -57,8 +57,21 @@ def test_is_masklen(): def test_is_netmask(): assert is_netmask('255.255.255.255') + assert is_netmask('0.0.0.0') + assert is_netmask('255.255.255.0') + assert is_netmask('255.255.254.0') assert not is_netmask(24) assert not is_netmask('foo') + # individually valid octets that are not a contiguous mask + assert not is_netmask('255.255.0.255') + assert not is_netmask('254.255.255.0') + assert not is_netmask('0.255.0.0') + assert not is_netmask('0.0.0.255') + # inet_aton-style legacy forms must stay rejected (they break to_masklen) + assert not is_netmask('0xffffff00') + assert not is_netmask('0377.0377.0377.0') + assert not is_netmask('4294967040') + assert not is_netmask('255.255.0') def test_to_ipv6_network():