Skip to content

Commit 18dcf92

Browse files
felixxmsarahboyce
authored andcommitted
[4.2.x] Refs CVE-2024-11168 -- Updated vendored _urlsplit() to properly validate IPv6 and IPvFuture addresses.
Refs Python CVE-2024-11168. Django should not affected, but others who incorrectly use internal function _urlsplit() with unsanitized input could be at risk. python/cpython#103849
1 parent 0acff0f commit 18dcf92

File tree

2 files changed

+60
-0
lines changed

2 files changed

+60
-0
lines changed

django/utils/http.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import base64
22
import datetime
3+
import ipaddress
34
import re
45
import unicodedata
56
from binascii import Error as BinasciiError
@@ -309,6 +310,21 @@ def _remove_unsafe_bytes_from_url(url):
309310
return url
310311

311312

313+
# TODO: Remove when dropping support for PY38.
314+
def _check_bracketed_host(hostname):
315+
# Valid bracketed hosts are defined in
316+
# https://www.rfc-editor.org/rfc/rfc3986#page-49 and
317+
# https://url.spec.whatwg.org/.
318+
if hostname.startswith("v"):
319+
if not re.match(r"\Av[a-fA-F0-9]+\..+\Z", hostname):
320+
raise ValueError("IPvFuture address is invalid")
321+
else:
322+
# Throws Value Error if not IPv6 or IPv4.
323+
ip = ipaddress.ip_address(hostname)
324+
if isinstance(ip, ipaddress.IPv4Address):
325+
raise ValueError("An IPv4 address cannot be in brackets")
326+
327+
312328
# TODO: Remove when dropping support for PY38.
313329
# Backport of urllib.parse.urlsplit() from Python 3.9.
314330
def _urlsplit(url, scheme="", allow_fragments=True):
@@ -336,6 +352,9 @@ def _urlsplit(url, scheme="", allow_fragments=True):
336352
"]" in netloc and "[" not in netloc
337353
):
338354
raise ValueError("Invalid IPv6 URL")
355+
if "[" in netloc and "]" in netloc:
356+
bracketed_host = netloc.partition("[")[2].partition("]")[0]
357+
_check_bracketed_host(bracketed_host)
339358
if allow_fragments and "#" in url:
340359
url, fragment = url.split("#", 1)
341360
if "?" in url:

tests/utils_tests/test_http.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from django.test import SimpleTestCase
77
from django.utils.datastructures import MultiValueDict
88
from django.utils.http import (
9+
_urlsplit,
910
base36_to_int,
1011
content_disposition_header,
1112
escape_leading_slashes,
@@ -291,6 +292,46 @@ def test_secure_param_non_https_urls(self):
291292
False,
292293
)
293294

295+
# TODO: Remove when dropping support for PY38.
296+
def test_invalid_bracketed_hosts(self):
297+
# Port of urllib.parse.urlsplit() tests from Python.
298+
tests = [
299+
"Scheme://user@[192.0.2.146]/Path?Query",
300+
"Scheme://user@[important.com:8000]/Path?Query",
301+
"Scheme://user@[v123r.IP]/Path?Query",
302+
"Scheme://user@[v12ae]/Path?Query",
303+
"Scheme://user@[v.IP]/Path?Query",
304+
"Scheme://user@[v123.]/Path?Query",
305+
"Scheme://user@[v]/Path?Query",
306+
"Scheme://user@[0439:23af::2309::fae7:1234]/Path?Query",
307+
"Scheme://user@[0439:23af:2309::fae7:1234:2342:438e:192.0.2.146]/"
308+
"Path?Query",
309+
"Scheme://user@]v6a.ip[/Path",
310+
]
311+
for invalid_url in tests:
312+
with self.subTest(invalid_url=invalid_url):
313+
self.assertRaises(ValueError, _urlsplit, invalid_url)
314+
315+
# TODO: Remove when dropping support for PY38.
316+
def test_splitting_bracketed_hosts(self):
317+
# Port of urllib.parse.urlsplit() tests from Python.
318+
p1 = _urlsplit("scheme://user@[v6a.ip]/path?query")
319+
self.assertEqual(p1.hostname, "v6a.ip")
320+
self.assertEqual(p1.username, "user")
321+
self.assertEqual(p1.path, "/path")
322+
# Removed the '%test' suffix from ported tests as %scope_id suffixes were
323+
# added in Python 3.9: https://docs.python.org/3/whatsnew/3.9.html#ipaddress
324+
p2 = _urlsplit("scheme://user@[0439:23af:2309::fae7]/path?query")
325+
self.assertEqual(p2.hostname, "0439:23af:2309::fae7")
326+
self.assertEqual(p2.username, "user")
327+
self.assertEqual(p2.path, "/path")
328+
p3 = _urlsplit(
329+
"scheme://user@[0439:23af:2309::fae7:1234:192.0.2.146]/path?query"
330+
)
331+
self.assertEqual(p3.hostname, "0439:23af:2309::fae7:1234:192.0.2.146")
332+
self.assertEqual(p3.username, "user")
333+
self.assertEqual(p3.path, "/path")
334+
294335

295336
class URLSafeBase64Tests(unittest.TestCase):
296337
def test_roundtrip(self):

0 commit comments

Comments
 (0)