Skip to content

Commit

Permalink
[4.2.x] Refs CVE-2024-11168 -- Updated vendored _urlsplit() to proper…
Browse files Browse the repository at this point in the history
…ly 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
  • Loading branch information
felixxm committed Dec 1, 2024
1 parent 0acff0f commit 28cf7c3
Show file tree
Hide file tree
Showing 2 changed files with 57 additions and 0 deletions.
19 changes: 19 additions & 0 deletions django/utils/http.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import base64
import datetime
import ipaddress
import re
import unicodedata
from binascii import Error as BinasciiError
Expand Down Expand Up @@ -309,6 +310,21 @@ def _remove_unsafe_bytes_from_url(url):
return url


# TODO: Remove when dropping support for PY38.
def _check_bracketed_host(hostname):
# Valid bracketed hosts are defined in
# https://www.rfc-editor.org/rfc/rfc3986#page-49 and
# https://url.spec.whatwg.org/.
if hostname.startswith("v"):
if not re.match(r"\Av[a-fA-F0-9]+\..+\Z", hostname):
raise ValueError("IPvFuture address is invalid")
else:
# Throws Value Error if not IPv6 or IPv4.
ip = ipaddress.ip_address(hostname)
if isinstance(ip, ipaddress.IPv4Address):
raise ValueError("An IPv4 address cannot be in brackets")


# TODO: Remove when dropping support for PY38.
# Backport of urllib.parse.urlsplit() from Python 3.9.
def _urlsplit(url, scheme="", allow_fragments=True):
Expand Down Expand Up @@ -336,6 +352,9 @@ def _urlsplit(url, scheme="", allow_fragments=True):
"]" in netloc and "[" not in netloc
):
raise ValueError("Invalid IPv6 URL")
if "[" in netloc and "]" in netloc:
bracketed_host = netloc.partition("[")[2].partition("]")[0]
_check_bracketed_host(bracketed_host)
if allow_fragments and "#" in url:
url, fragment = url.split("#", 1)
if "?" in url:
Expand Down
38 changes: 38 additions & 0 deletions tests/utils_tests/test_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from django.test import SimpleTestCase
from django.utils.datastructures import MultiValueDict
from django.utils.http import (
_urlsplit,
base36_to_int,
content_disposition_header,
escape_leading_slashes,
Expand Down Expand Up @@ -291,6 +292,43 @@ def test_secure_param_non_https_urls(self):
False,
)

# TODO: Remove when dropping support for PY38.
def test_invalid_bracketed_hosts(self):
# Port of urllib.parse.urlsplit() tests from Python.
tests = [
"Scheme://user@[192.0.2.146]/Path?Query",
"Scheme://user@[important.com:8000]/Path?Query",
"Scheme://user@[v123r.IP]/Path?Query",
"Scheme://user@[v12ae]/Path?Query",
"Scheme://user@[v.IP]/Path?Query",
"Scheme://user@[v123.]/Path?Query",
"Scheme://user@[v]/Path?Query",
"Scheme://user@[0439:23af::2309::fae7:1234]/Path?Query",
"Scheme://user@[0439:23af:2309::fae7:1234:2342:438e:192.0.2.146]/Path?Query",
"Scheme://user@]v6a.ip[/Path",
]
for invalid_url in tests:
with self.subTest(invalid_url=invalid_url):
self.assertRaises(ValueError, _urlsplit, invalid_url)

# TODO: Remove when dropping support for PY38.
def test_splitting_bracketed_hosts(self):
# Port of urllib.parse.urlsplit() tests from Python.
p1 = _urlsplit("scheme://user@[v6a.ip]/path?query")
self.assertEqual(p1.hostname, "v6a.ip")
self.assertEqual(p1.username, "user")
self.assertEqual(p1.path, "/path")
p2 = _urlsplit("scheme://user@[0439:23af:2309::fae7%test]/path?query")
self.assertEqual(p2.hostname, "0439:23af:2309::fae7%test")
self.assertEqual(p2.username, "user")
self.assertEqual(p2.path, "/path")
p3 = _urlsplit(
"scheme://user@[0439:23af:2309::fae7:1234:192.0.2.146%test]/path?query"
)
self.assertEqual(p3.hostname, "0439:23af:2309::fae7:1234:192.0.2.146%test")
self.assertEqual(p3.username, "user")
self.assertEqual(p3.path, "/path")


class URLSafeBase64Tests(unittest.TestCase):
def test_roundtrip(self):
Expand Down

0 comments on commit 28cf7c3

Please sign in to comment.