From 115bec4b27fb23c917f1a33f50727c422c52a15e Mon Sep 17 00:00:00 2001 From: Seth Michael Larson Date: Tue, 14 Jan 2025 11:13:33 -0600 Subject: [PATCH 1/6] GH-128840: Limit the number of parts in IPv6 address parsing --- Lib/ipaddress.py | 7 +++++-- Lib/test/test_ipaddress.py | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Lib/ipaddress.py b/Lib/ipaddress.py index 703fa289dda1fb..135b35d023a4f1 100644 --- a/Lib/ipaddress.py +++ b/Lib/ipaddress.py @@ -1661,7 +1661,11 @@ def _ip_int_from_string(cls, ip_str): if not ip_str: raise AddressValueError('Address cannot be empty') - parts = ip_str.split(':') + # We want to allow more parts than the max to be 'split' + # to preserve the correct error message when there are + # too many parts combined with '::' + _max_parts = cls._HEXTET_COUNT + 1 + parts = ip_str.split(':', maxsplit=_max_parts) # An IPv6 address needs at least 2 colons (3 parts). _min_parts = 3 @@ -1681,7 +1685,6 @@ def _ip_int_from_string(cls, ip_str): # An IPv6 address can't have more than 8 colons (9 parts). # The extra colon comes from using the "::" notation for a single # leading or trailing zero part. - _max_parts = cls._HEXTET_COUNT + 1 if len(parts) > _max_parts: msg = "At most %d colons permitted in %r" % (_max_parts-1, ip_str) raise AddressValueError(msg) diff --git a/Lib/test/test_ipaddress.py b/Lib/test/test_ipaddress.py index b1ac2b94f41b38..c1b672caba2d94 100644 --- a/Lib/test/test_ipaddress.py +++ b/Lib/test/test_ipaddress.py @@ -416,6 +416,8 @@ def assertBadSplit(addr): # A trailing IPv4 address is two parts assertBadSplit("9:8:7:6:5:4:3:42.42.42.42%scope") assertBadSplit("7:6:5:4:3:42.42.42.42%scope") + # Long IPv6 address + assertBadSplit(("0:" * 10000) + "0") def test_bad_address_split_v6_too_many_parts_with_double_colon(self): def assertBadSplit(addr): From 0afa53888deb87a727e2ba2235c1c0077a366ea8 Mon Sep 17 00:00:00 2001 From: Seth Michael Larson Date: Tue, 14 Jan 2025 11:19:16 -0600 Subject: [PATCH 2/6] Add news entry --- .../next/Security/2025-01-14-11-19-07.gh-issue-128840.M1doZW.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Security/2025-01-14-11-19-07.gh-issue-128840.M1doZW.rst diff --git a/Misc/NEWS.d/next/Security/2025-01-14-11-19-07.gh-issue-128840.M1doZW.rst b/Misc/NEWS.d/next/Security/2025-01-14-11-19-07.gh-issue-128840.M1doZW.rst new file mode 100644 index 00000000000000..e197ee9c6677d8 --- /dev/null +++ b/Misc/NEWS.d/next/Security/2025-01-14-11-19-07.gh-issue-128840.M1doZW.rst @@ -0,0 +1 @@ +Limit the number of parts split during IPv6 address parsing. From 3e055b46a472d86a279d3e3976d951f5025390af Mon Sep 17 00:00:00 2001 From: Seth Michael Larson Date: Tue, 14 Jan 2025 12:14:18 -0600 Subject: [PATCH 3/6] Fix location of new test case --- Lib/test/test_ipaddress.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_ipaddress.py b/Lib/test/test_ipaddress.py index c1b672caba2d94..37aa3b3fc706d1 100644 --- a/Lib/test/test_ipaddress.py +++ b/Lib/test/test_ipaddress.py @@ -396,6 +396,8 @@ def assertBadSplit(addr): assertBadSplit("8:7:6:5:4:3:2:1::%scope") # A trailing IPv4 address is two parts assertBadSplit("10:9:8:7:6:5:4:3:42.42.42.42%scope") + # Long IPv6 address + assertBadSplit(("0:" * 10000) + "0") def test_bad_address_split_v6_too_many_parts(self): def assertBadSplit(addr): @@ -416,8 +418,6 @@ def assertBadSplit(addr): # A trailing IPv4 address is two parts assertBadSplit("9:8:7:6:5:4:3:42.42.42.42%scope") assertBadSplit("7:6:5:4:3:42.42.42.42%scope") - # Long IPv6 address - assertBadSplit(("0:" * 10000) + "0") def test_bad_address_split_v6_too_many_parts_with_double_colon(self): def assertBadSplit(addr): From e65928735aae806d1343d7b14802fb6dd0fb6b64 Mon Sep 17 00:00:00 2001 From: Seth Michael Larson Date: Tue, 14 Jan 2025 16:10:29 -0600 Subject: [PATCH 4/6] Update with more descriptive news --- .../Security/2025-01-14-11-19-07.gh-issue-128840.M1doZW.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Security/2025-01-14-11-19-07.gh-issue-128840.M1doZW.rst b/Misc/NEWS.d/next/Security/2025-01-14-11-19-07.gh-issue-128840.M1doZW.rst index e197ee9c6677d8..fbfeb355262bbb 100644 --- a/Misc/NEWS.d/next/Security/2025-01-14-11-19-07.gh-issue-128840.M1doZW.rst +++ b/Misc/NEWS.d/next/Security/2025-01-14-11-19-07.gh-issue-128840.M1doZW.rst @@ -1 +1,3 @@ -Limit the number of parts split during IPv6 address parsing. +Limit the number of splitting on colons (``:``) that will occur while parsing +an IPv6 address. This prevents excessive memory consumption and potential +denial-of-service when parsing a large IPv6 address. From e1e69174a279157e0be73b045ae302381b954e4e Mon Sep 17 00:00:00 2001 From: Seth Michael Larson Date: Wed, 15 Jan 2025 10:49:49 -0600 Subject: [PATCH 5/6] Limit length of IP address string to 39 --- Lib/ipaddress.py | 3 +++ Lib/test/test_ipaddress.py | 11 ++++++++++- .../2025-01-14-11-19-07.gh-issue-128840.M1doZW.rst | 5 ++--- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/Lib/ipaddress.py b/Lib/ipaddress.py index 135b35d023a4f1..67a8d578a238ce 100644 --- a/Lib/ipaddress.py +++ b/Lib/ipaddress.py @@ -1660,6 +1660,9 @@ def _ip_int_from_string(cls, ip_str): """ if not ip_str: raise AddressValueError('Address cannot be empty') + if len(ip_str) > 39: + msg = "At most 39 characters expected in %r" % (ip_str,) + raise AddressValueError(msg) # We want to allow more parts than the max to be 'split' # to preserve the correct error message when there are diff --git a/Lib/test/test_ipaddress.py b/Lib/test/test_ipaddress.py index 37aa3b3fc706d1..21602de4024369 100644 --- a/Lib/test/test_ipaddress.py +++ b/Lib/test/test_ipaddress.py @@ -396,8 +396,17 @@ def assertBadSplit(addr): assertBadSplit("8:7:6:5:4:3:2:1::%scope") # A trailing IPv4 address is two parts assertBadSplit("10:9:8:7:6:5:4:3:42.42.42.42%scope") + + def test_bad_address_split_v6_too_long(self): + def assertBadSplit(addr): + msg = "At most 39 characters expected in %r" + with self.assertAddressError(msg, addr.split('%')[0]): + ipaddress.IPv6Address(addr) + # Long IPv6 address - assertBadSplit(("0:" * 10000) + "0") + long_addr = ("0:" * 10000) + "0" + assertBadSplit(long_addr) + assertBadSplit(long_addr + "%zoneid") def test_bad_address_split_v6_too_many_parts(self): def assertBadSplit(addr): diff --git a/Misc/NEWS.d/next/Security/2025-01-14-11-19-07.gh-issue-128840.M1doZW.rst b/Misc/NEWS.d/next/Security/2025-01-14-11-19-07.gh-issue-128840.M1doZW.rst index fbfeb355262bbb..362b43c67ee83b 100644 --- a/Misc/NEWS.d/next/Security/2025-01-14-11-19-07.gh-issue-128840.M1doZW.rst +++ b/Misc/NEWS.d/next/Security/2025-01-14-11-19-07.gh-issue-128840.M1doZW.rst @@ -1,3 +1,2 @@ -Limit the number of splitting on colons (``:``) that will occur while parsing -an IPv6 address. This prevents excessive memory consumption and potential -denial-of-service when parsing a large IPv6 address. +Short-circuit the processing of long IPv6 addresses early to prevent excessive +memory consumption and a minor denial-of-service. From 9401977fbbb1c18ea678bbb2ba6e3fa0f944f687 Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" Date: Fri, 23 May 2025 19:27:37 -0700 Subject: [PATCH 6/6] Elide the looong value in the exception message. --- Lib/ipaddress.py | 3 ++- Lib/test/test_ipaddress.py | 4 ++-- .../Security/2025-01-14-11-19-07.gh-issue-128840.M1doZW.rst | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Lib/ipaddress.py b/Lib/ipaddress.py index bee7fb3463b671..69e933a6541f78 100644 --- a/Lib/ipaddress.py +++ b/Lib/ipaddress.py @@ -1661,7 +1661,8 @@ def _ip_int_from_string(cls, ip_str): if not ip_str: raise AddressValueError('Address cannot be empty') if len(ip_str) > 39: - msg = "At most 39 characters expected in %r" % (ip_str,) + msg = ("At most 39 characters expected in " + f"{ip_str[:14]!r}({len(ip_str)-28} chars elided){ip_str[-14:]!r}") raise AddressValueError(msg) # We want to allow more parts than the max to be 'split' diff --git a/Lib/test/test_ipaddress.py b/Lib/test/test_ipaddress.py index d95b87a7463dfe..ee95454e64bb0d 100644 --- a/Lib/test/test_ipaddress.py +++ b/Lib/test/test_ipaddress.py @@ -399,8 +399,8 @@ def assertBadSplit(addr): def test_bad_address_split_v6_too_long(self): def assertBadSplit(addr): - msg = "At most 39 characters expected in %r" - with self.assertAddressError(msg, addr.split('%')[0]): + msg = r"At most 39 characters expected in %s" + with self.assertAddressError(msg, repr(re.escape(addr[:14]))): ipaddress.IPv6Address(addr) # Long IPv6 address diff --git a/Misc/NEWS.d/next/Security/2025-01-14-11-19-07.gh-issue-128840.M1doZW.rst b/Misc/NEWS.d/next/Security/2025-01-14-11-19-07.gh-issue-128840.M1doZW.rst index 362b43c67ee83b..b57ec3e70dcc5f 100644 --- a/Misc/NEWS.d/next/Security/2025-01-14-11-19-07.gh-issue-128840.M1doZW.rst +++ b/Misc/NEWS.d/next/Security/2025-01-14-11-19-07.gh-issue-128840.M1doZW.rst @@ -1,2 +1,2 @@ -Short-circuit the processing of long IPv6 addresses early to prevent excessive +Short-circuit the processing of long IPv6 addresses early in :mod:`ipaddress` to prevent excessive memory consumption and a minor denial-of-service.