Skip to content

Commit 4f2496b

Browse files
mceplJamesJohnUtley
andcommitted
[CVE-2024-9287] ensure that bracketed hosts found by urlsplit are of IPv6 or IPvFuture format
Fix urlparse incorrectly retrieves IPv4 and regular name hosts from inside of brackets Reproducer is python3 -c \ 'from urllib.parse import urlparse; print(urlparse("https://user:some]password[@host.com"))' This command should fail with the error "ValueError: '@host.com' does not appear to be an IPv4 or IPv6 address". If it doesn’t and produces ParseResult(scheme='https', netloc='user:some]password[@host.com', path='', params='', query='', fragment='') it is this bug. Fixes: bsc#1233307 (CVE-2024-11168) Fixes: gh#python#103848 Co-authored-by: JohnJamesUtley <[email protected]> From-PR: gh#python/cpython!103849 Patch: CVE-2024-11168-validation-IPv6-addrs.patch
1 parent c9571a5 commit 4f2496b

File tree

4 files changed

+68
-2
lines changed

4 files changed

+68
-2
lines changed

Lib/ipaddress.py

+25-1
Original file line numberDiff line numberDiff line change
@@ -1886,12 +1886,32 @@ def max_prefixlen(self):
18861886
def version(self):
18871887
return self._version
18881888

1889+
@staticmethod
1890+
def _split_scope_id(ip_str):
1891+
"""Helper function to parse IPv6 string address with scope id.
1892+
1893+
See RFC 4007 for details.
1894+
1895+
Args:
1896+
ip_str: A string, the IPv6 address.
1897+
1898+
Returns:
1899+
(addr, scope_id) tuple.
1900+
1901+
"""
1902+
addr, sep, scope_id = ip_str.partition('%')
1903+
if not sep:
1904+
scope_id = None
1905+
elif not scope_id or '%' in scope_id:
1906+
raise AddressValueError('Invalid IPv6 address: "%r"' % ip_str)
1907+
return addr, scope_id
1908+
18891909

18901910
class IPv6Address(_BaseV6, _BaseAddress):
18911911

18921912
"""Represent and manipulate single IPv6 Addresses."""
18931913

1894-
__slots__ = ('_ip', '__weakref__')
1914+
__slots__ = ('_ip', '_scope_id', '__weakref__')
18951915

18961916
def __init__(self, address):
18971917
"""Instantiate a new IPv6 address object.
@@ -1914,19 +1934,23 @@ def __init__(self, address):
19141934
if isinstance(address, int):
19151935
self._check_int_address(address)
19161936
self._ip = address
1937+
self._scope_id = None
19171938
return
19181939

19191940
# Constructing from a packed address
19201941
if isinstance(address, bytes):
19211942
self._check_packed_address(address, 16)
19221943
self._ip = int.from_bytes(address, 'big')
1944+
self._scope_id = None
19231945
return
19241946

19251947
# Assume input argument to be string or any object representation
19261948
# which converts into a formatted IP string.
19271949
addr_str = str(address)
19281950
if '/' in addr_str:
19291951
raise AddressValueError("Unexpected '/' in %r" % address)
1952+
addr_str, self._scope_id = self._split_scope_id(addr_str)
1953+
19301954
self._ip = self._ip_int_from_string(addr_str)
19311955

19321956
@property

Lib/test/test_urlparse.py

+26
Original file line numberDiff line numberDiff line change
@@ -1035,6 +1035,32 @@ def test_issue14072(self):
10351035
self.assertEqual(p2.scheme, 'tel')
10361036
self.assertEqual(p2.path, '+31641044153')
10371037

1038+
def test_invalid_bracketed_hosts(self):
1039+
self.assertRaises(ValueError, urllib.parse.urlsplit, 'Scheme://user@[192.0.2.146]/Path?Query')
1040+
self.assertRaises(ValueError, urllib.parse.urlsplit, 'Scheme://user@[important.com:8000]/Path?Query')
1041+
self.assertRaises(ValueError, urllib.parse.urlsplit, 'Scheme://user@[v123r.IP]/Path?Query')
1042+
self.assertRaises(ValueError, urllib.parse.urlsplit, 'Scheme://user@[v12ae]/Path?Query')
1043+
self.assertRaises(ValueError, urllib.parse.urlsplit, 'Scheme://user@[v.IP]/Path?Query')
1044+
self.assertRaises(ValueError, urllib.parse.urlsplit, 'Scheme://user@[v123.]/Path?Query')
1045+
self.assertRaises(ValueError, urllib.parse.urlsplit, 'Scheme://user@[v]/Path?Query')
1046+
self.assertRaises(ValueError, urllib.parse.urlsplit, 'Scheme://user@[0439:23af::2309::fae7:1234]/Path?Query')
1047+
self.assertRaises(ValueError, urllib.parse.urlsplit, 'Scheme://user@[0439:23af:2309::fae7:1234:2342:438e:192.0.2.146]/Path?Query')
1048+
self.assertRaises(ValueError, urllib.parse.urlsplit, 'Scheme://user@]v6a.ip[/Path')
1049+
1050+
def test_splitting_bracketed_hosts(self):
1051+
p1 = urllib.parse.urlsplit('scheme://user@[v6a.ip]/path?query')
1052+
self.assertEqual(p1.hostname, 'v6a.ip')
1053+
self.assertEqual(p1.username, 'user')
1054+
self.assertEqual(p1.path, '/path')
1055+
p2 = urllib.parse.urlsplit('scheme://user@[0439:23af:2309::fae7%test]/path?query')
1056+
self.assertEqual(p2.hostname, '0439:23af:2309::fae7%test')
1057+
self.assertEqual(p2.username, 'user')
1058+
self.assertEqual(p2.path, '/path')
1059+
p3 = urllib.parse.urlsplit('scheme://user@[0439:23af:2309::fae7:1234:192.0.2.146%test]/path?query')
1060+
self.assertEqual(p3.hostname, '0439:23af:2309::fae7:1234:192.0.2.146%test')
1061+
self.assertEqual(p3.username, 'user')
1062+
self.assertEqual(p3.path, '/path')
1063+
10381064
def test_telurl_params(self):
10391065
p1 = urllib.parse.urlparse('tel:123-4;phone-context=+1-650-516')
10401066
self.assertEqual(p1.scheme, 'tel')

Lib/urllib/parse.py

+15-1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import re
3131
import sys
3232
import collections
33+
import ipaddress
3334

3435
__all__ = ["urlparse", "urlunparse", "urljoin", "urldefrag",
3536
"urlsplit", "urlunsplit", "urlencode", "parse_qs",
@@ -417,6 +418,17 @@ def _checknetloc(netloc):
417418
raise ValueError("netloc '" + netloc + "' contains invalid " +
418419
"characters under NFKC normalization")
419420

421+
# Valid bracketed hosts are defined in
422+
# https://www.rfc-editor.org/rfc/rfc3986#page-49 and https://url.spec.whatwg.org/
423+
def _check_bracketed_host(hostname):
424+
if hostname.startswith('v'):
425+
if not re.match(r"\Av[a-fA-F0-9]+\..+\Z", hostname):
426+
raise ValueError(f"IPvFuture address is invalid")
427+
else:
428+
ip = ipaddress.ip_address(hostname) # Throws Value Error if not IPv6 or IPv4
429+
if isinstance(ip, ipaddress.IPv4Address):
430+
raise ValueError(f"An IPv4 address cannot be in brackets")
431+
420432
def _remove_unsafe_bytes_from_url(url):
421433
for b in _UNSAFE_URL_BYTES_TO_REMOVE:
422434
url = url.replace(b, "")
@@ -467,12 +479,14 @@ def urlsplit(url, scheme='', allow_fragments=True):
467479
if not rest or any(c not in '0123456789' for c in rest):
468480
# not a port number
469481
scheme, url = url[:i].lower(), rest
470-
471482
if url[:2] == '//':
472483
netloc, url = _splitnetloc(url, 2)
473484
if (('[' in netloc and ']' not in netloc) or
474485
(']' in netloc and '[' not in netloc)):
475486
raise ValueError("Invalid IPv6 URL")
487+
if '[' in netloc and ']' in netloc:
488+
bracketed_host = netloc.partition('[')[2].partition(']')[0]
489+
_check_bracketed_host(bracketed_host)
476490
if allow_fragments and '#' in url:
477491
url, fragment = url.split('#', 1)
478492
if '?' in url:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add checks to ensure that ``[`` bracketed ``]`` hosts found by
2+
:func:`urllib.parse.urlsplit` are of IPv6 or IPvFuture format.

0 commit comments

Comments
 (0)