diff --git a/django/__init__.py b/django/__init__.py
index a9428ad60237..dddc14f597a5 100644
--- a/django/__init__.py
+++ b/django/__init__.py
@@ -1,6 +1,6 @@
from django.utils.version import get_version
-VERSION = (4, 2, 18, "final", 0)
+VERSION = (4, 2, 20, "final", 0)
__version__ = get_version(VERSION)
diff --git a/django/core/validators.py b/django/core/validators.py
index 4a5835dbb0d4..c5886039380d 100644
--- a/django/core/validators.py
+++ b/django/core/validators.py
@@ -272,6 +272,8 @@ def __eq__(self, other):
def validate_ipv4_address(value):
+ if isinstance(value, ipaddress.IPv4Address):
+ return
try:
ipaddress.IPv4Address(value)
except ValueError:
diff --git a/django/utils/ipv6.py b/django/utils/ipv6.py
index de41a97f7210..487fa7b1e102 100644
--- a/django/utils/ipv6.py
+++ b/django/utils/ipv6.py
@@ -49,12 +49,14 @@ def clean_ipv6_address(
return str(addr)
-def is_valid_ipv6_address(ip_str):
+def is_valid_ipv6_address(ip_addr):
"""
- Return whether or not the `ip_str` string is a valid IPv6 address.
+ Return whether the `ip_addr` object is a valid IPv6 address.
"""
+ if isinstance(ip_addr, ipaddress.IPv6Address):
+ return True
try:
- _ipv6_address_from_str(ip_str)
- except ValueError:
+ _ipv6_address_from_str(ip_addr)
+ except (TypeError, ValueError):
return False
return True
diff --git a/django/utils/text.py b/django/utils/text.py
index e1b835e0e219..81ae88dc76d4 100644
--- a/django/utils/text.py
+++ b/django/utils/text.py
@@ -1,6 +1,7 @@
import gzip
import re
import secrets
+import textwrap
import unicodedata
from gzip import GzipFile
from gzip import compress as gzip_compress
@@ -97,24 +98,15 @@ def wrap(text, width):
``width``.
"""
- def _generator():
- for line in text.splitlines(True): # True keeps trailing linebreaks
- max_width = min((line.endswith("\n") and width + 1 or width), width)
- while len(line) > max_width:
- space = line[: max_width + 1].rfind(" ") + 1
- if space == 0:
- space = line.find(" ") + 1
- if space == 0:
- yield line
- line = ""
- break
- yield "%s\n" % line[: space - 1]
- line = line[space:]
- max_width = min((line.endswith("\n") and width + 1 or width), width)
- if line:
- yield line
-
- return "".join(_generator())
+ wrapper = textwrap.TextWrapper(
+ width=width,
+ break_long_words=False,
+ break_on_hyphens=False,
+ )
+ result = []
+ for line in text.splitlines(True):
+ result.extend(wrapper.wrap(line))
+ return "\n".join(result)
class Truncator(SimpleLazyObject):
diff --git a/docs/releases/4.2.19.txt b/docs/releases/4.2.19.txt
new file mode 100644
index 000000000000..9bb2d3ed52dd
--- /dev/null
+++ b/docs/releases/4.2.19.txt
@@ -0,0 +1,14 @@
+===========================
+Django 4.2.19 release notes
+===========================
+
+*February 5, 2025*
+
+Django 4.2.19 fixes a regression in 4.2.18.
+
+Bugfixes
+========
+
+* Fixed a regression in Django 4.2.18 that caused ``validate_ipv6_address()``
+ and ``validate_ipv46_address()`` to crash when handling non-string values
+ (:ticket:`36098`).
diff --git a/docs/releases/4.2.20.txt b/docs/releases/4.2.20.txt
new file mode 100644
index 000000000000..5849fe2a42ed
--- /dev/null
+++ b/docs/releases/4.2.20.txt
@@ -0,0 +1,13 @@
+===========================
+Django 4.2.20 release notes
+===========================
+
+*March 6, 2025*
+
+Django 4.2.20 fixes a security issue with severity "moderate" in 4.2.19.
+
+CVE-2025-26699: Potential denial-of-service vulnerability in ``django.utils.text.wrap()``
+=========================================================================================
+
+The ``wrap()`` and :tfilter:`wordwrap` template filter were subject to a
+potential denial-of-service attack when used with very long strings.
diff --git a/docs/releases/index.txt b/docs/releases/index.txt
index ec69c8e414bb..00e4465845f8 100644
--- a/docs/releases/index.txt
+++ b/docs/releases/index.txt
@@ -26,6 +26,8 @@ versions of the documentation contain the release notes for any later releases.
.. toctree::
:maxdepth: 1
+ 4.2.20
+ 4.2.19
4.2.18
4.2.17
4.2.16
diff --git a/docs/releases/security.txt b/docs/releases/security.txt
index 95a6e003b217..f997fe94a3a3 100644
--- a/docs/releases/security.txt
+++ b/docs/releases/security.txt
@@ -36,6 +36,17 @@ Issues under Django's security process
All security issues have been handled under versions of Django's security
process. These are listed below.
+January 14, 2025 - :cve:`2024-56374`
+------------------------------------
+
+Potential denial-of-service vulnerability in IPv6 validation.
+`Full description
+`__
+
+* Django 5.1 :commit:`(patch) <4806731e58f3e8700a3c802e77899d54ac6021fe>`
+* Django 5.0 :commit:`(patch) `
+* Django 4.2 :commit:`(patch) `
+
December 4, 2024 - :cve:`2024-53907`
------------------------------------
diff --git a/tests/model_fields/test_jsonfield.py b/tests/model_fields/test_jsonfield.py
index 4c8d14bf9a17..8535aa2e8c7f 100644
--- a/tests/model_fields/test_jsonfield.py
+++ b/tests/model_fields/test_jsonfield.py
@@ -611,7 +611,7 @@ def test_has_key_deep(self):
def test_has_key_literal_lookup(self):
self.assertSequenceEqual(
NullableJSONModel.objects.filter(
- HasKey(Value({"foo": "bar"}, JSONField()), "foo")
+ HasKey(Cast(Value({"foo": "bar"}, JSONField()), JSONField()), "foo")
).order_by("id"),
self.objs,
)
diff --git a/tests/template_tests/filter_tests/test_wordwrap.py b/tests/template_tests/filter_tests/test_wordwrap.py
index 88fbd274da94..4afa1dd234f1 100644
--- a/tests/template_tests/filter_tests/test_wordwrap.py
+++ b/tests/template_tests/filter_tests/test_wordwrap.py
@@ -78,3 +78,14 @@ def test_wrap_lazy_string(self):
"this is a long\nparagraph of\ntext that\nreally needs\nto be wrapped\n"
"I'm afraid",
)
+
+ def test_wrap_long_text(self):
+ long_text = (
+ "this is a long paragraph of text that really needs"
+ " to be wrapped I'm afraid " * 20_000
+ )
+ self.assertIn(
+ "this is a\nlong\nparagraph\nof text\nthat\nreally\nneeds to\nbe wrapped\n"
+ "I'm afraid",
+ wordwrap(long_text, 10),
+ )
diff --git a/tests/utils_tests/test_ipv6.py b/tests/utils_tests/test_ipv6.py
index 2d06507fa152..3734222318f4 100644
--- a/tests/utils_tests/test_ipv6.py
+++ b/tests/utils_tests/test_ipv6.py
@@ -1,5 +1,7 @@
import traceback
+from decimal import Decimal
from io import StringIO
+from ipaddress import IPv6Address
from django.core.exceptions import ValidationError
from django.test import SimpleTestCase
@@ -24,6 +26,16 @@ def test_validates_correct_with_v4mapping(self):
self.assertTrue(is_valid_ipv6_address("::ffff:254.42.16.14"))
self.assertTrue(is_valid_ipv6_address("::ffff:0a0a:0a0a"))
+ def test_validates_correct_with_ipv6_instance(self):
+ cases = [
+ IPv6Address("::ffff:2.125.160.216"),
+ IPv6Address("fe80::1"),
+ IPv6Address("::"),
+ ]
+ for case in cases:
+ with self.subTest(case=case):
+ self.assertIs(is_valid_ipv6_address(case), True)
+
def test_validates_incorrect_plain_address(self):
self.assertFalse(is_valid_ipv6_address("foo"))
self.assertFalse(is_valid_ipv6_address("127.0.0.1"))
@@ -46,6 +58,12 @@ def test_validates_incorrect_with_v4mapping(self):
self.assertFalse(is_valid_ipv6_address("::999.42.16.14"))
self.assertFalse(is_valid_ipv6_address("::zzzz:0a0a"))
+ def test_validates_incorrect_with_non_string(self):
+ cases = [None, [], {}, (), Decimal("2.46"), 192.168, 42]
+ for case in cases:
+ with self.subTest(case=case):
+ self.assertIs(is_valid_ipv6_address(case), False)
+
def test_cleans_plain_address(self):
self.assertEqual(clean_ipv6_address("DEAD::0:BEEF"), "dead::beef")
self.assertEqual(
diff --git a/tests/validators/tests.py b/tests/validators/tests.py
index e99baab862d9..f4ab7fbece2f 100644
--- a/tests/validators/tests.py
+++ b/tests/validators/tests.py
@@ -1,3 +1,4 @@
+import ipaddress
import re
import types
from datetime import datetime, timedelta
@@ -381,15 +382,25 @@
(validate_ipv6_address, "fe80::1", None),
(validate_ipv6_address, "::1", None),
(validate_ipv6_address, "1:2:3:4:5:6:7:8", None),
+ (validate_ipv6_address, ipaddress.IPv6Address("::ffff:2.125.160.216"), None),
+ (validate_ipv6_address, ipaddress.IPv6Address("fe80::1"), None),
+ (validate_ipv6_address, Decimal("33.1"), ValidationError),
+ (validate_ipv6_address, 9.22, ValidationError),
(validate_ipv6_address, "1:2", ValidationError),
(validate_ipv6_address, "::zzz", ValidationError),
(validate_ipv6_address, "12345::", ValidationError),
(validate_ipv46_address, "1.1.1.1", None),
(validate_ipv46_address, "255.0.0.0", None),
(validate_ipv46_address, "0.0.0.0", None),
+ (validate_ipv46_address, ipaddress.IPv4Address("1.1.1.1"), None),
+ (validate_ipv46_address, ipaddress.IPv4Address("255.0.0.0"), None),
(validate_ipv46_address, "fe80::1", None),
(validate_ipv46_address, "::1", None),
(validate_ipv46_address, "1:2:3:4:5:6:7:8", None),
+ (validate_ipv46_address, ipaddress.IPv6Address("::ffff:2.125.160.216"), None),
+ (validate_ipv46_address, ipaddress.IPv6Address("fe80::1"), None),
+ (validate_ipv46_address, Decimal("33.1"), ValidationError),
+ (validate_ipv46_address, 9.22, ValidationError),
(validate_ipv46_address, "256.1.1.1", ValidationError),
(validate_ipv46_address, "25.1.1.", ValidationError),
(validate_ipv46_address, "25,1,1,1", ValidationError),