CVE-2024-4032: Insufficient Information (NVD-CWE-noinfo)
value: The “ipaddress” module contained incorrect information about whether certain IPv4 and IPv6 addresses were designated as “globally reachable” or “private”. This affected the is_private and is_global properties of the ipaddress.IPv4Address, ipaddress.IPv4Network, ipaddress.IPv6Address, and ipaddress.IPv6Network classes, where values wouldn’t be returned in accordance with the latest information from the IANA Special-Purpose Address Registries.

CPython 3.12.4 and 3.13.0a6 contain updated information from these registries and thus have the intended behavior.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-06-17T15:15Z
Repository: https://github.com/python/cpython
Language: Other
Files changed: Doc/library/ipaddress.rst, Doc/whatsnew/3.13.rst, Lib/ipaddress.py, Lib/test/test_ipaddress.py, Misc/NEWS.d/next/Library/2024-03-14-01-38-44.gh-issue-113171.VFnObz.rst
Commit date: 2024-03-22 17:49:56 +0100
Commit message: GH-113171: Fix "private" (non-global) IP address ranges (GH-113179)

* GH-113171: Fix "private" (really non-global) IP address ranges

The _private_networks variables, used by various is_private
implementations, were missing some ranges and at the same time had
overly strict ranges (where there are more specific ranges considered
globally reachable by the IANA registries).

This patch updates the ranges with what was missing or otherwise
incorrect.

I left 100.64.0.0/10 alone, for now, as it's been made special in [1]
and I'm not sure if we want to undo that as I don't quite understand the
motivation behind it.

The _address_exclude_many() call returns 8 networks for IPv4, 121
networks for IPv6.

[1] https://github.com/python/cpython/issues/61602

Diff: diff --git a/Doc/library/ipaddress.rst b/Doc/library/ipaddress.rst
index 73f496008261..8f090b5eec59 100644
--- a/Doc/library/ipaddress.rst
+++ b/Doc/library/ipaddress.rst
@@ -190,10 +190,22 @@ write code that handles both IP versions correctly.  Address objects are
             address.is_private == address.ipv4_mapped.is_private
 
       ``is_private`` has value opposite to :attr:`is_global`, except for the shared address space
       (``100.64.0.0/10`` range) where they are both ``False``.
 
+      .. versionchanged:: 3.13
+
+         Fixed some false positives and false negatives.
+
+         * ``192.0.0.0/24`` is considered private with the exception of ``192.0.0.9/32`` and
+           ``192.0.0.10/32`` (previously: only the ``192.0.0.0/29`` sub-range was considered private).
+         * ``64:ff9b:1::/48`` is considered private.
+         * ``2002::/16`` is considered private.
+         * There are exceptions within ``2001::/23`` (otherwise considered private): ``2001:1::1/128``,
+           ``2001:1::2/128``, ``2001:3::/32``, ``2001:4:112::/48``, ``2001:20::/28``, ``2001:30::/28``.
+           The exceptions are not considered private.
+
    .. attribute:: is_global
 
       ``True`` if the address is defined as globally reachable by
       iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
       (for IPv6) with the following exception:
@@ -207,10 +219,14 @@ write code that handles both IP versions correctly.  Address objects are
       ``is_global`` has value opposite to :attr:`is_private`, except for the shared address space
       (``100.64.0.0/10`` range) where they are both ``False``.
 
       .. versionadded:: 3.4
 
+      .. versionchanged:: 3.13
+
+         Fixed some false positives and false negatives, see :attr:`is_private` for details.
+
    .. attribute:: is_unspecified
 
       ``True`` if the address is unspecified.  See :RFC:`5735` (for IPv4)
       or :RFC:`2373` (for IPv6).
 
diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst
index 7e6c79dbf50a..bec788e7ed2b 100644
--- a/Doc/whatsnew/3.13.rst
+++ b/Doc/whatsnew/3.13.rst
@@ -399,10 +399,12 @@ built on debug mode <debug-build>`.
 ipaddress
 ---------
 
 * Add the :attr:`ipaddress.IPv4Address.ipv6_mapped` property, which returns the IPv4-mapped IPv6 address.
   (Contributed by Charles Machalow in :gh:`109466`.)
+* Fix ``is_global`` and ``is_private`` behavior in ``IPv4Address``, ``IPv6Address``, ``IPv4Network``
+  and ``IPv6Network``.
 
 itertools
 ---------
 
 * Added a ``strict`` option to :func:`itertools.batched`.
diff --git a/Lib/ipaddress.py b/Lib/ipaddress.py
index 7d6edcf2478a..22cdfc93d8ad 100644
--- a/Lib/ipaddress.py
+++ b/Lib/ipaddress.py
@@ -1084,11 +1084,15 @@ def is_private(self):
             iana-ipv4-special-registry or iana-ipv6-special-registry.
 
         """
         return any(self.network_address in priv_network and
                    self.broadcast_address in priv_network
-                   for priv_network in self._constants._private_networks)
+                   for priv_network in self._constants._private_networks) and all(
+                    self.network_address not in network and
+                    self.broadcast_address not in network
+                    for network in self._constants._private_networks_exceptions
+                )
 
     @property
     def is_global(self):
         """Test if this address is allocated for public networks.
 
@@ -1345,11 +1349,14 @@ def is_private(self):
                 address.is_private == address.ipv4_mapped.is_private
 
         ``is_private`` has value opposite to :attr:`is_global`, except for the ``100.64.0.0/10``
         IPv4 range where they are both ``False``.
         """
-        return any(self in net for net in self._constants._private_networks)
+        return (
+            any(self in net for net in self._constants._private_networks)
+            and all(self not in net for net in self._constants._private_networks_exceptions)
+        )
 
     @property
     @functools.lru_cache()
     def is_global(self):
         """``True`` if the address is defined as globally reachable by
@@ -1576,27 +1583,34 @@ class _IPv4Constants:
 
     _multicast_network = IPv4Network('224.0.0.0/4')
 
     _public_network = IPv4Network('100.64.0.0/10')
 
+    # Not globally reachable address blocks listed on
+    # https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
     _private_networks = [
         IPv4Network('0.0.0.0/8'),
         IPv4Network('10.0.0.0/8'),
         IPv4Network('127.0.0.0/8'),
         IPv4Network('169.254.0.0/16'),
         IPv4Network('172.16.0.0/12'),
-        IPv4Network('192.0.0.0/29'),
+        IPv4Network('192.0.0.0/24'),
         IPv4Network('192.0.0.170/31'),
         IPv4Network('192.0.2.0/24'),
         IPv4Network('192.168.0.0/16'),
         IPv4Network('198.18.0.0/15'),
         IPv4Network('198.51.100.0/24'),
         IPv4Network('203.0.113.0/24'),
         IPv4Network('240.0.0.0/4'),
         IPv4Network('255.255.255.255/32'),
         ]
 
+    _private_networks_exceptions = [
+        IPv4Network('192.0.0.9/32'),
+        IPv4Network('192.0.0.10/32'),
+    ]
+
     _reserved_network = IPv4Network('240.0.0.0/4')
 
     _unspecified_address = IPv4Address('0.0.0.0')
 
 
@@ -2084,11 +2098,14 @@ def is_private(self):
         IPv4 range where they are both ``False``.
         """
         ipv4_mapped = self.ipv4_mapped
         if ipv4_mapped is not None:
             return ipv4_mapped.is_private
-        return any(self in net for net in self._constants._private_networks)
+        return (
+            any(self in net for net in self._constants._private_networks)
+            and all(self not in net for net in self._constants._private_networks_exceptions)
+        )
 
     @property
     def is_global(self):
         """``True`` if the address is defined as globally reachable by
         iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
@@ -2340,23 +2357,35 @@ class _IPv6Constants:
 
     _linklocal_network = IPv6Network('fe80::/10')
 
     _multicast_network = IPv6Network('ff00::/8')
 
+    # Not globally reachable address blocks listed on
+    # https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
     _private_networks = [
         IPv6Network('::1/128'),
         IPv6Network('::/128'),
         IPv6Network('::ffff:0:0/96'),
+        IPv6Network('64:ff9b:1::/48'),
         IPv6Network('100::/64'),
         IPv6Network('2001::/23'),
-        IPv6Network('2001:2::/48'),
         IPv6Network('2001:db8::/32'),
-        IPv6Network('2001:10::/28'),
+        # IANA says N/A, let's consider it not globally reachable to be safe
+        IPv6Network('2002::/16'),
         IPv6Network('fc00::/7'),
         IPv6Network('fe80::/10'),
         ]
 
+    _private_networks_exceptions = [
+        IPv6Network('2001:1::1/128'),
+        IPv6Network('2001:1::2/128'),
+        IPv6Network('2001:3::/32'),
+        IPv6Network('2001:4:112::/48'),
+        IPv6Network('2001:20::/28'),
+        IPv6Network('2001:30::/28'),
+    ]
+
     _reserved_networks = [
         IPv6Network('::/8'), IPv6Network('100::/8'),
         IPv6Network('200::/7'), IPv6Network('400::/6'),
         IPv6Network('800::/5'), IPv6Network('1000::/4'),
         IPv6Network('4000::/3'), IPv6Network('6000::/3'),
diff --git a/Lib/test/test_ipaddress.py b/Lib/test/test_ipaddress.py
index b4952acc2b61..f1519df67374 100644
--- a/Lib/test/test_ipaddress.py
+++ b/Lib/test/test_ipaddress.py
@@ -2286,10 +2286,14 @@ def testReservedIpv4(self):
                 '10.255.255.255').is_private)
         self.assertEqual(False, ipaddress.ip_address('11.0.0.0').is_private)
         self.assertEqual(True, ipaddress.ip_address(
                 '172.31.255.255').is_private)
         self.assertEqual(False, ipaddress.ip_address('172.32.0.0').is_private)
+        self.assertFalse(ipaddress.ip_address('192.0.0.0').is_global)
+        self.assertTrue(ipaddress.ip_address('192.0.0.9').is_global)
+        self.assertTrue(ipaddress.ip_address('192.0.0.10').is_global)
+        self.assertFalse(ipaddress.ip_address('192.0.0.255').is_global)
 
         self.assertEqual(True,
                          ipaddress.ip_address('169.254.100.200').is_link_local)
         self.assertEqual(False,
                          ipaddress.ip_address('169.255.100.200').is_link_local)
@@ -2311,10 +2315,11 @@ def testPrivateNetworks(self):
         self.assertEqual(True, ipaddress.ip_network("10.0.0.0/8").is_private)
         self.assertEqual(True, ipaddress.ip_network("127.0.0.0/8").is_private)
         self.assertEqual(True, ipaddress.ip_network("169.254.0.0/16").is_private)
         self.assertEqual(True, ipaddress.ip_network("172.16.0.0/12").is_private)
         self.assertEqual(True, ipaddress.ip_network("192.0.0.0/29").is_private)
+        self.assertEqual(False, ipaddress.ip_network("192.0.0.9/32").is_private)
         self.assertEqual(True, ipaddress.ip_network("192.0.0.170/31").is_private)
         self.assertEqual(True, ipaddress.ip_network("192.0.2.0/24").is_private)
         self.assertEqual(True, ipaddress.ip_network("192.168.0.0/16").is_private)
         self.assertEqual(True, ipaddress.ip_network("198.18.0.0/15").is_private)
         self.assertEqual(True, ipaddress.ip_network("198.51.100.0/24").is_private)
@@ -2327,12 +2332,12 @@ def testPrivateNetworks(self):
 
         self.assertEqual(True, ipaddress.ip_network("::1/128").is_private)
         self.assertEqual(True, ipaddress.ip_network("::/128").is_private)
         self.assertEqual(True, ipaddress.ip_network("::ffff:0:0/96").is_private)
         self.assertEqual(True, ipaddress.ip_network("100::/64").is_private)
-        self.assertEqual(True, ipaddress.ip_network("2001::/23").is_private)
         self.assertEqual(True, ipaddress.ip_network("2001:2::/48").is_private)
+        self.assertEqual(False, ipaddress.ip_network("2001:3::/48").is_private)
         self.assertEqual(True, ipaddress.ip_network("2001:db8::/32").is_private)
         self.assertEqual(True, ipaddress.ip_network("2001:10::/28").is_private)
         self.assertEqual(True, ipaddress.ip_network("fc00::/7").is_private)
         self.assertEqual(True, ipaddress.ip_network("fe80::/10").is_private)
 
@@ -2407,10 +2412,24 @@ def testReservedIpv6(self):
         self.assertEqual(False, ipaddress.ip_address('::2').is_loopback)
 
         self.assertEqual(True, ipaddress.ip_address('0::0').is_unspecified)
         self.assertEqual(False, ipaddress.ip_address('::1').is_unspecified)
 
+        self.assertFalse(ipaddress.ip_address('64:ff9b:1::').is_global)
+        self.assertFalse(ipaddress.ip_address('2001::').is_global)
+        self.assertTrue(ipaddress.ip_address('2001:1::1').is_global)
+        self.assertTrue(ipaddress.ip_address('2001:1::2').is_global)
+        self.assertFalse(ipaddress.ip_address('2001:2::').is_global)
+        self.assertTrue(ipaddress.ip_address('2001:3::').is_global)
+        self.assertFalse(ipaddress.ip_address('2001:4::').is_global)
+        self.assertTrue(ipaddress.ip_address('2001:4:112::').is_global)
+        self.assertFalse(ipaddress.ip_address('2001:10::').is_global)
+        self.assertTrue(ipaddress.ip_address('2001:20::').is_global)
+        self.assertTrue(ipaddress.ip_address('2001:30::').is_global)
+        self.assertFalse(ipaddress.ip_address('2001:40::').is_global)
+        self.assertFalse(ipaddress.ip_address('2002::').is_global)
+
         # some generic IETF reserved addresses
         self.assertEqual(True, ipaddress.ip_address('100::').is_reserved)
         self.assertEqual(True, ipaddress.ip_network('4000::1/128').is_reserved)
 
     def testIpv4Mapped(self):
diff --git a/Misc/NEWS.d/next/Library/2024-03-14-01-38-44.gh-issue-113171.VFnObz.rst b/Misc/NEWS.d/next/Library/2024-03-14-01-38-44.gh-issue-113171.VFnObz.rst
new file mode 100644
index 000000000000..f9a72473be4e
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-03-14-01-38-44.gh-issue-113171.VFnObz.rst
@@ -0,0 +1,9 @@
+Fixed various false positives and false negatives in
+
+* :attr:`ipaddress.IPv4Address.is_private` (see these docs for details)
+* :attr:`ipaddress.IPv4Address.is_global`
+* :attr:`ipaddress.IPv6Address.is_private`
+* :attr:`ipaddress.IPv6Address.is_global`
+
+Also in the corresponding :class:`ipaddress.IPv4Network` and :class:`ipaddress.IPv6Network`
+attributes.

Vulnerable code:                    for priv_network in self._constants._private_networks)
        return any(self in net for net in self._constants._private_networks)
        IPv4Network('192.0.0.0/29'),
        return any(self in net for net in self._constants._private_networks)
        IPv6Network('2001:2::/48'),
        IPv6Network('2001:10::/28'),
        self.assertEqual(True, ipaddress.ip_network("2001::/23").is_private)
Fixed code:       .. versionchanged:: 3.13

         Fixed some false positives and false negatives.

         * ``192.0.0.0/24`` is considered private with the exception of ``192.0.0.9/32`` and
           ``192.0.0.10/32`` (previously: only the ``192.0.0.0/29`` sub-range was considered private).
         * ``64:ff9b:1::/48`` is considered private.
         * ``2002::/16`` is considered private.
         * There are exceptions within ``2001::/23`` (otherwise considered private): ``2001:1::1/128``,
           ``2001:1::2/128``, ``2001:3::/32``, ``2001:4:112::/48``, ``2001:20::/28``, ``2001:30::/28``.
           The exceptions are not considered private.

      .. versionchanged:: 3.13

         Fixed some false positives and false negatives, see :attr:`is_private` for details.

* Fix ``is_global`` and ``is_private`` behavior in ``IPv4Address``, ``IPv6Address``, ``IPv4Network``
  and ``IPv6Network``.
                   for priv_network in self._constants._private_networks) and all(
                    self.network_address not in network and
                    self.broadcast_address not in network
                    for network in self._constants._private_networks_exceptions
                )
        return (
            any(self in net for net in self._constants._private_networks)
            and all(self not in net for net in self._constants._private_networks_exceptions)
        )
    # Not globally reachable address blocks listed on
    # https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
        IPv4Network('192.0.0.0/24'),
    _private_networks_exceptions = [
        IPv4Network('192.0.0.9/32'),
        IPv4Network('192.0.0.10/32'),
    ]

        return (
            any(self in net for net in self._constants._private_networks)
            and all(self not in net for net in self._constants._private_networks_exceptions)
        )
    # Not globally reachable address blocks listed on
    # https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
        IPv6Network('64:ff9b:1::/48'),
        # IANA says N/A, let's consider it not globally reachable to be safe
        IPv6Network('2002::/16'),
    _private_networks_exceptions = [
        IPv6Network('2001:1::1/128'),
        IPv6Network('2001:1::2/128'),
        IPv6Network('2001:3::/32'),
        IPv6Network('2001:4:112::/48'),
        IPv6Network('2001:20::/28'),
        IPv6Network('2001:30::/28'),
    ]

        self.assertFalse(ipaddress.ip_address('192.0.0.0').is_global)
        self.assertTrue(ipaddress.ip_address('192.0.0.9').is_global)
        self.assertTrue(ipaddress.ip_address('192.0.0.10').is_global)
        self.assertFalse(ipaddress.ip_address('192.0.0.255').is_global)
        self.assertEqual(False, ipaddress.ip_network("192.0.0.9/32").is_private)
        self.assertEqual(False, ipaddress.ip_network("2001:3::/48").is_private)
        self.assertFalse(ipaddress.ip_address('64:ff9b:1::').is_global)
        self.assertFalse(ipaddress.ip_address('2001::').is_global)
        self.assertTrue(ipaddress.ip_address('2001:1::1').is_global)
        self.assertTrue(ipaddress.ip_address('2001:1::2').is_global)
        self.assertFalse(ipaddress.ip_address('2001:2::').is_global)
        self.assertTrue(ipaddress.ip_address('2001:3::').is_global)
        self.assertFalse(ipaddress.ip_address('2001:4::').is_global)
        self.assertTrue(ipaddress.ip_address('2001:4:112::').is_global)
        self.assertFalse(ipaddress.ip_address('2001:10::').is_global)
        self.assertTrue(ipaddress.ip_address('2001:20::').is_global)
        self.assertTrue(ipaddress.ip_address('2001:30::').is_global)
        self.assertFalse(ipaddress.ip_address('2001:40::').is_global)
        self.assertFalse(ipaddress.ip_address('2002::').is_global)

Fixed various false positives and false negatives in

* :attr:`ipaddress.IPv4Address.is_private` (see these docs for details)
* :attr:`ipaddress.IPv4Address.is_global`
* :attr:`ipaddress.IPv6Address.is_private`
* :attr:`ipaddress.IPv6Address.is_global`

Also in the corresponding :class:`ipaddress.IPv4Network` and :class:`ipaddress.IPv6Network`
attributes.