diff options
| author | Saravanan <saravanan.kadambathursubramaniyam@windriver.com> | 2025-11-30 17:11:29 +0530 |
|---|---|---|
| committer | Gyorgy Sarvari <skandigraun@gmail.com> | 2025-11-30 15:16:31 +0100 |
| commit | 0b554678b68189e14293a8a6a07bb6998ce345c4 (patch) | |
| tree | c1f08279ef62012579042972e52b711c31ff9c32 /meta-python | |
| parent | 540b79e3eeec268bfbc49ad1fe227cc385005bf4 (diff) | |
| download | meta-openembedded-0b554678b68189e14293a8a6a07bb6998ce345c4.tar.gz | |
python3-django: fix CVE-2024-56374
Reference:
https://nvd.nist.gov/vuln/detail/CVE-2024-56374
Upstream-patch:
https://github.com/django/django/commit/ad866a1ca3e7d60da888d25d27e46a8adb2ed36e
Signed-off-by: Saravanan <saravanan.kadambathursubramaniyam@windriver.com>
Signed-off-by: Gyorgy Sarvari <skandigraun@gmail.com>
Diffstat (limited to 'meta-python')
4 files changed, 625 insertions, 0 deletions
diff --git a/meta-python/recipes-devtools/python/python3-django-3.2.25/CVE-2024-56374.patch b/meta-python/recipes-devtools/python/python3-django-3.2.25/CVE-2024-56374.patch new file mode 100644 index 0000000000..90ab279624 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-django-3.2.25/CVE-2024-56374.patch | |||
| @@ -0,0 +1,308 @@ | |||
| 1 | From ad866a1ca3e7d60da888d25d27e46a8adb2ed36e Mon Sep 17 00:00:00 2001 | ||
| 2 | From: Natalia <124304+nessita@users.noreply.github.com> | ||
| 3 | Date: Mon, 6 Jan 2025 15:51:45 -0300 | ||
| 4 | Subject: [PATCH] Fixed CVE-2024-56374 -- Mitigated potential DoS in IPv6 | ||
| 5 | validation. | ||
| 6 | |||
| 7 | Thanks Saravana Kumar for the report, and Sarah Boyce and Mariusz | ||
| 8 | Felisiak for the reviews. | ||
| 9 | |||
| 10 | CVE: CVE-2024-56374 | ||
| 11 | |||
| 12 | Upstream-Status: Backport | ||
| 13 | https://github.com/django/django/commit/ad866a1ca3e7d60da888d25d27e46a8adb2ed36e | ||
| 14 | |||
| 15 | Signed-off-by: Natalia <124304+nessita@users.noreply.github.com> | ||
| 16 | Co-authored-by: Natalia <124304+nessita@users.noreply.github.com> | ||
| 17 | Signed-off-by: Saravanan <saravanan.kadambathursubramaniyam@windriver.com> | ||
| 18 | --- | ||
| 19 | django/db/models/fields/__init__.py | 4 +- | ||
| 20 | django/forms/fields.py | 7 +++- | ||
| 21 | django/utils/ipv6.py | 22 ++++++++-- | ||
| 22 | docs/ref/forms/fields.txt | 13 +++++- | ||
| 23 | docs/releases/3.2.25.txt | 12 ++++++ | ||
| 24 | .../field_tests/test_genericipaddressfield.py | 35 +++++++++++++++- | ||
| 25 | tests/utils_tests/test_ipv6.py | 40 +++++++++++++++++-- | ||
| 26 | 7 files changed, 119 insertions(+), 14 deletions(-) | ||
| 27 | |||
| 28 | diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py | ||
| 29 | index 167c3d2..148201d 100644 | ||
| 30 | --- a/django/db/models/fields/__init__.py | ||
| 31 | +++ b/django/db/models/fields/__init__.py | ||
| 32 | @@ -22,7 +22,7 @@ from django.utils.dateparse import ( | ||
| 33 | ) | ||
| 34 | from django.utils.duration import duration_microseconds, duration_string | ||
| 35 | from django.utils.functional import Promise, cached_property | ||
| 36 | -from django.utils.ipv6 import clean_ipv6_address | ||
| 37 | +from django.utils.ipv6 import MAX_IPV6_ADDRESS_LENGTH, clean_ipv6_address | ||
| 38 | from django.utils.itercompat import is_iterable | ||
| 39 | from django.utils.text import capfirst | ||
| 40 | from django.utils.translation import gettext_lazy as _ | ||
| 41 | @@ -1940,7 +1940,7 @@ class GenericIPAddressField(Field): | ||
| 42 | kwargs['unpack_ipv4'] = self.unpack_ipv4 | ||
| 43 | if self.protocol != "both": | ||
| 44 | kwargs['protocol'] = self.protocol | ||
| 45 | - if kwargs.get("max_length") == 39: | ||
| 46 | + if kwargs.get("max_length") == self.max_length: | ||
| 47 | del kwargs['max_length'] | ||
| 48 | return name, path, args, kwargs | ||
| 49 | |||
| 50 | diff --git a/django/forms/fields.py b/django/forms/fields.py | ||
| 51 | index 8adb09e..6969c4a 100644 | ||
| 52 | --- a/django/forms/fields.py | ||
| 53 | +++ b/django/forms/fields.py | ||
| 54 | @@ -28,7 +28,7 @@ from django.forms.widgets import ( | ||
| 55 | from django.utils import formats | ||
| 56 | from django.utils.dateparse import parse_datetime, parse_duration | ||
| 57 | from django.utils.duration import duration_string | ||
| 58 | -from django.utils.ipv6 import clean_ipv6_address | ||
| 59 | +from django.utils.ipv6 import MAX_IPV6_ADDRESS_LENGTH, clean_ipv6_address | ||
| 60 | from django.utils.regex_helper import _lazy_re_compile | ||
| 61 | from django.utils.translation import gettext_lazy as _, ngettext_lazy | ||
| 62 | |||
| 63 | @@ -1179,6 +1179,7 @@ class GenericIPAddressField(CharField): | ||
| 64 | def __init__(self, *, protocol='both', unpack_ipv4=False, **kwargs): | ||
| 65 | self.unpack_ipv4 = unpack_ipv4 | ||
| 66 | self.default_validators = validators.ip_address_validators(protocol, unpack_ipv4)[0] | ||
| 67 | + kwargs.setdefault("max_length", MAX_IPV6_ADDRESS_LENGTH) | ||
| 68 | super().__init__(**kwargs) | ||
| 69 | |||
| 70 | def to_python(self, value): | ||
| 71 | @@ -1186,7 +1187,9 @@ class GenericIPAddressField(CharField): | ||
| 72 | return '' | ||
| 73 | value = value.strip() | ||
| 74 | if value and ':' in value: | ||
| 75 | - return clean_ipv6_address(value, self.unpack_ipv4) | ||
| 76 | + return clean_ipv6_address( | ||
| 77 | + value, self.unpack_ipv4, max_length=self.max_length | ||
| 78 | + ) | ||
| 79 | return value | ||
| 80 | |||
| 81 | |||
| 82 | diff --git a/django/utils/ipv6.py b/django/utils/ipv6.py | ||
| 83 | index ddb8c80..aed7902 100644 | ||
| 84 | --- a/django/utils/ipv6.py | ||
| 85 | +++ b/django/utils/ipv6.py | ||
| 86 | @@ -3,9 +3,23 @@ import ipaddress | ||
| 87 | from django.core.exceptions import ValidationError | ||
| 88 | from django.utils.translation import gettext_lazy as _ | ||
| 89 | |||
| 90 | +MAX_IPV6_ADDRESS_LENGTH = 39 | ||
| 91 | |||
| 92 | -def clean_ipv6_address(ip_str, unpack_ipv4=False, | ||
| 93 | - error_message=_("This is not a valid IPv6 address.")): | ||
| 94 | + | ||
| 95 | +def _ipv6_address_from_str(ip_str, max_length=MAX_IPV6_ADDRESS_LENGTH): | ||
| 96 | + if len(ip_str) > max_length: | ||
| 97 | + raise ValueError( | ||
| 98 | + f"Unable to convert {ip_str} to an IPv6 address (value too long)." | ||
| 99 | + ) | ||
| 100 | + return ipaddress.IPv6Address(int(ipaddress.IPv6Address(ip_str))) | ||
| 101 | + | ||
| 102 | + | ||
| 103 | +def clean_ipv6_address( | ||
| 104 | + ip_str, | ||
| 105 | + unpack_ipv4=False, | ||
| 106 | + error_message=_("This is not a valid IPv6 address."), | ||
| 107 | + max_length=MAX_IPV6_ADDRESS_LENGTH, | ||
| 108 | + ): | ||
| 109 | """ | ||
| 110 | Clean an IPv6 address string. | ||
| 111 | |||
| 112 | @@ -23,7 +37,7 @@ def clean_ipv6_address(ip_str, unpack_ipv4=False, | ||
| 113 | Return a compressed IPv6 address or the same value. | ||
| 114 | """ | ||
| 115 | try: | ||
| 116 | - addr = ipaddress.IPv6Address(int(ipaddress.IPv6Address(ip_str))) | ||
| 117 | + addr = _ipv6_address_from_str(ip_str, max_length) | ||
| 118 | except ValueError: | ||
| 119 | raise ValidationError(error_message, code='invalid') | ||
| 120 | |||
| 121 | @@ -40,7 +54,7 @@ def is_valid_ipv6_address(ip_str): | ||
| 122 | Return whether or not the `ip_str` string is a valid IPv6 address. | ||
| 123 | """ | ||
| 124 | try: | ||
| 125 | - ipaddress.IPv6Address(ip_str) | ||
| 126 | + _ipv6_address_from_str(ip_str) | ||
| 127 | except ValueError: | ||
| 128 | return False | ||
| 129 | return True | ||
| 130 | diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt | ||
| 131 | index 5b485f2..45973eb 100644 | ||
| 132 | --- a/docs/ref/forms/fields.txt | ||
| 133 | +++ b/docs/ref/forms/fields.txt | ||
| 134 | @@ -847,7 +847,7 @@ For each field, we describe the default widget used if you don't specify | ||
| 135 | * Empty value: ``''`` (an empty string) | ||
| 136 | * Normalizes to: A string. IPv6 addresses are normalized as described below. | ||
| 137 | * Validates that the given value is a valid IP address. | ||
| 138 | - * Error message keys: ``required``, ``invalid`` | ||
| 139 | + * Error message keys: ``required``, ``invalid``, ``max_length`` | ||
| 140 | |||
| 141 | The IPv6 address normalization follows :rfc:`4291#section-2.2` section 2.2, | ||
| 142 | including using the IPv4 format suggested in paragraph 3 of that section, like | ||
| 143 | @@ -855,7 +855,7 @@ For each field, we describe the default widget used if you don't specify | ||
| 144 | ``2001::1``, and ``::ffff:0a0a:0a0a`` to ``::ffff:10.10.10.10``. All characters | ||
| 145 | are converted to lowercase. | ||
| 146 | |||
| 147 | - Takes two optional arguments: | ||
| 148 | + Takes three optional arguments: | ||
| 149 | |||
| 150 | .. attribute:: protocol | ||
| 151 | |||
| 152 | @@ -870,6 +870,15 @@ For each field, we describe the default widget used if you don't specify | ||
| 153 | ``192.0.2.1``. Default is disabled. Can only be used | ||
| 154 | when ``protocol`` is set to ``'both'``. | ||
| 155 | |||
| 156 | + .. attribute:: max_length | ||
| 157 | + | ||
| 158 | + Defaults to 39, and behaves the same way as it does for | ||
| 159 | + :class:`CharField`. | ||
| 160 | + | ||
| 161 | + .. versionchanged:: 4.2.18 | ||
| 162 | + | ||
| 163 | + The default value for ``max_length`` was set to 39 characters. | ||
| 164 | + | ||
| 165 | ``MultipleChoiceField`` | ||
| 166 | ----------------------- | ||
| 167 | |||
| 168 | diff --git a/docs/releases/3.2.25.txt b/docs/releases/3.2.25.txt | ||
| 169 | index f8d9ce2..93ab341 100644 | ||
| 170 | --- a/docs/releases/3.2.25.txt | ||
| 171 | +++ b/docs/releases/3.2.25.txt | ||
| 172 | @@ -21,6 +21,18 @@ CVE-2025-26699: Potential denial-of-service vulnerability in ``django.utils.text | ||
| 173 | The ``wrap()`` and :tfilter:`wordwrap` template filter were subject to a | ||
| 174 | potential denial-of-service attack when used with very long strings. | ||
| 175 | |||
| 176 | +CVE-2024-56374: Potential denial-of-service vulnerability in IPv6 validation | ||
| 177 | +============================================================================ | ||
| 178 | + | ||
| 179 | +Lack of upper bound limit enforcement in strings passed when performing IPv6 | ||
| 180 | +validation could lead to a potential denial-of-service attack. The undocumented | ||
| 181 | +and private functions ``clean_ipv6_address`` and ``is_valid_ipv6_address`` were | ||
| 182 | +vulnerable, as was the :class:`django.forms.GenericIPAddressField` form field, | ||
| 183 | +which has now been updated to define a ``max_length`` of 39 characters. | ||
| 184 | + | ||
| 185 | +The :class:`django.db.models.GenericIPAddressField` model field was not | ||
| 186 | +affected. | ||
| 187 | + | ||
| 188 | Bugfixes | ||
| 189 | ======== | ||
| 190 | |||
| 191 | diff --git a/tests/forms_tests/field_tests/test_genericipaddressfield.py b/tests/forms_tests/field_tests/test_genericipaddressfield.py | ||
| 192 | index 92dbd71..fc3f129 100644 | ||
| 193 | --- a/tests/forms_tests/field_tests/test_genericipaddressfield.py | ||
| 194 | +++ b/tests/forms_tests/field_tests/test_genericipaddressfield.py | ||
| 195 | @@ -1,6 +1,7 @@ | ||
| 196 | from django.core.exceptions import ValidationError | ||
| 197 | from django.forms import GenericIPAddressField | ||
| 198 | from django.test import SimpleTestCase | ||
| 199 | +from django.utils.ipv6 import MAX_IPV6_ADDRESS_LENGTH | ||
| 200 | |||
| 201 | |||
| 202 | class GenericIPAddressFieldTest(SimpleTestCase): | ||
| 203 | @@ -90,6 +91,35 @@ class GenericIPAddressFieldTest(SimpleTestCase): | ||
| 204 | with self.assertRaisesMessage(ValidationError, "'This is not a valid IPv6 address.'"): | ||
| 205 | f.clean('1:2') | ||
| 206 | |||
| 207 | + def test_generic_ipaddress_max_length_custom(self): | ||
| 208 | + # Valid IPv4-mapped IPv6 address, len 45. | ||
| 209 | + addr = "0000:0000:0000:0000:0000:ffff:192.168.100.228" | ||
| 210 | + f = GenericIPAddressField(max_length=len(addr)) | ||
| 211 | + f.clean(addr) | ||
| 212 | + | ||
| 213 | + def test_generic_ipaddress_max_length_validation_error(self): | ||
| 214 | + # Valid IPv4-mapped IPv6 address, len 45. | ||
| 215 | + addr = "0000:0000:0000:0000:0000:ffff:192.168.100.228" | ||
| 216 | + | ||
| 217 | + cases = [ | ||
| 218 | + ({}, MAX_IPV6_ADDRESS_LENGTH), # Default value. | ||
| 219 | + ({"max_length": len(addr) - 1}, len(addr) - 1), | ||
| 220 | + ] | ||
| 221 | + for kwargs, max_length in cases: | ||
| 222 | + max_length_plus_one = max_length + 1 | ||
| 223 | + msg = ( | ||
| 224 | + f"Ensure this value has at most {max_length} characters (it has " | ||
| 225 | + f"{max_length_plus_one}).'" | ||
| 226 | + ) | ||
| 227 | + with self.subTest(max_length=max_length): | ||
| 228 | + f = GenericIPAddressField(**kwargs) | ||
| 229 | + with self.assertRaisesMessage(ValidationError, msg): | ||
| 230 | + f.clean("x" * max_length_plus_one) | ||
| 231 | + with self.assertRaisesMessage( | ||
| 232 | + ValidationError, "This is not a valid IPv6 address." | ||
| 233 | + ): | ||
| 234 | + f.clean(addr) | ||
| 235 | + | ||
| 236 | def test_generic_ipaddress_as_generic_not_required(self): | ||
| 237 | f = GenericIPAddressField(required=False) | ||
| 238 | self.assertEqual(f.clean(''), '') | ||
| 239 | @@ -104,7 +134,10 @@ class GenericIPAddressFieldTest(SimpleTestCase): | ||
| 240 | with self.assertRaisesMessage(ValidationError, "'Enter a valid IPv4 or IPv6 address.'"): | ||
| 241 | f.clean('256.125.1.5') | ||
| 242 | self.assertEqual(f.clean(' fe80::223:6cff:fe8a:2e8a '), 'fe80::223:6cff:fe8a:2e8a') | ||
| 243 | - self.assertEqual(f.clean(' 2a02::223:6cff:fe8a:2e8a '), '2a02::223:6cff:fe8a:2e8a') | ||
| 244 | + self.assertEqual( | ||
| 245 | + f.clean(" " * MAX_IPV6_ADDRESS_LENGTH + " 2a02::223:6cff:fe8a:2e8a "), | ||
| 246 | + "2a02::223:6cff:fe8a:2e8a", | ||
| 247 | + ) | ||
| 248 | with self.assertRaisesMessage(ValidationError, "'This is not a valid IPv6 address.'"): | ||
| 249 | f.clean('12345:2:3:4') | ||
| 250 | with self.assertRaisesMessage(ValidationError, "'This is not a valid IPv6 address.'"): | ||
| 251 | diff --git a/tests/utils_tests/test_ipv6.py b/tests/utils_tests/test_ipv6.py | ||
| 252 | index 4e434f3..1ac6763 100644 | ||
| 253 | --- a/tests/utils_tests/test_ipv6.py | ||
| 254 | +++ b/tests/utils_tests/test_ipv6.py | ||
| 255 | @@ -1,9 +1,17 @@ | ||
| 256 | -import unittest | ||
| 257 | +import traceback | ||
| 258 | +from io import StringIO | ||
| 259 | |||
| 260 | -from django.utils.ipv6 import clean_ipv6_address, is_valid_ipv6_address | ||
| 261 | +from django.core.exceptions import ValidationError | ||
| 262 | +from django.test import SimpleTestCase | ||
| 263 | +from django.utils.ipv6 import ( | ||
| 264 | + MAX_IPV6_ADDRESS_LENGTH, | ||
| 265 | + clean_ipv6_address, | ||
| 266 | + is_valid_ipv6_address, | ||
| 267 | +) | ||
| 268 | +from django.utils.version import PY310 | ||
| 269 | |||
| 270 | |||
| 271 | -class TestUtilsIPv6(unittest.TestCase): | ||
| 272 | +class TestUtilsIPv6(SimpleTestCase): | ||
| 273 | |||
| 274 | def test_validates_correct_plain_address(self): | ||
| 275 | self.assertTrue(is_valid_ipv6_address('fe80::223:6cff:fe8a:2e8a')) | ||
| 276 | @@ -55,3 +63,29 @@ class TestUtilsIPv6(unittest.TestCase): | ||
| 277 | self.assertEqual(clean_ipv6_address('::ffff:0a0a:0a0a', unpack_ipv4=True), '10.10.10.10') | ||
| 278 | self.assertEqual(clean_ipv6_address('::ffff:1234:1234', unpack_ipv4=True), '18.52.18.52') | ||
| 279 | self.assertEqual(clean_ipv6_address('::ffff:18.52.18.52', unpack_ipv4=True), '18.52.18.52') | ||
| 280 | + | ||
| 281 | + def test_address_too_long(self): | ||
| 282 | + addresses = [ | ||
| 283 | + "0000:0000:0000:0000:0000:ffff:192.168.100.228", # IPv4-mapped IPv6 address | ||
| 284 | + "0000:0000:0000:0000:0000:ffff:192.168.100.228%123456", # % scope/zone | ||
| 285 | + "fe80::223:6cff:fe8a:2e8a:1234:5678:00000", # MAX_IPV6_ADDRESS_LENGTH + 1 | ||
| 286 | + ] | ||
| 287 | + msg = "This is the error message." | ||
| 288 | + value_error_msg = "Unable to convert %s to an IPv6 address (value too long)." | ||
| 289 | + for addr in addresses: | ||
| 290 | + with self.subTest(addr=addr): | ||
| 291 | + self.assertGreater(len(addr), MAX_IPV6_ADDRESS_LENGTH) | ||
| 292 | + self.assertEqual(is_valid_ipv6_address(addr), False) | ||
| 293 | + with self.assertRaisesMessage(ValidationError, msg) as ctx: | ||
| 294 | + clean_ipv6_address(addr, error_message=msg) | ||
| 295 | + exception_traceback = StringIO() | ||
| 296 | + if PY310: | ||
| 297 | + traceback.print_exception(ctx.exception, file=exception_traceback) | ||
| 298 | + else: | ||
| 299 | + traceback.print_exception( | ||
| 300 | + type(ctx.exception), | ||
| 301 | + value=ctx.exception, | ||
| 302 | + tb=ctx.exception.__traceback__, | ||
| 303 | + file=exception_traceback, | ||
| 304 | + ) | ||
| 305 | + self.assertIn(value_error_msg % addr, exception_traceback.getvalue()) | ||
| 306 | -- | ||
| 307 | 2.40.0 | ||
| 308 | |||
diff --git a/meta-python/recipes-devtools/python/python3-django/CVE-2024-56374.patch b/meta-python/recipes-devtools/python/python3-django/CVE-2024-56374.patch new file mode 100644 index 0000000000..3b86eacc41 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-django/CVE-2024-56374.patch | |||
| @@ -0,0 +1,315 @@ | |||
| 1 | From ad866a1ca3e7d60da888d25d27e46a8adb2ed36e Mon Sep 17 00:00:00 2001 | ||
| 2 | From: Natalia <124304+nessita@users.noreply.github.com> | ||
| 3 | Date: Mon, 6 Jan 2025 15:51:45 -0300 | ||
| 4 | Subject: [PATCH] Fixed CVE-2024-56374 -- Mitigated potential DoS in IPv6 | ||
| 5 | validation. | ||
| 6 | |||
| 7 | Thanks Saravana Kumar for the report, and Sarah Boyce and Mariusz | ||
| 8 | Felisiak for the reviews. | ||
| 9 | |||
| 10 | CVE: CVE-2024-56374 | ||
| 11 | |||
| 12 | Upstream-Status: Backport | ||
| 13 | https://github.com/django/django/commit/ad866a1ca3e7d60da888d25d27e46a8adb2ed36e | ||
| 14 | |||
| 15 | Signed-off-by: Natalia <124304+nessita@users.noreply.github.com> | ||
| 16 | Co-authored-by: Natalia <124304+nessita@users.noreply.github.com> | ||
| 17 | Signed-off-by: Saravanan <saravanan.kadambathursubramaniyam@windriver.com> | ||
| 18 | |||
| 19 | %% original patch: CVE-2024-56374.patch | ||
| 20 | --- | ||
| 21 | django/db/models/fields/__init__.py | 6 +-- | ||
| 22 | django/forms/fields.py | 7 +++- | ||
| 23 | django/utils/ipv6.py | 22 ++++++++-- | ||
| 24 | docs/ref/forms/fields.txt | 13 +++++- | ||
| 25 | docs/releases/2.2.28.txt | 12 ++++++ | ||
| 26 | .../field_tests/test_genericipaddressfield.py | 35 +++++++++++++++- | ||
| 27 | tests/utils_tests/test_ipv6.py | 40 +++++++++++++++++-- | ||
| 28 | 7 files changed, 120 insertions(+), 15 deletions(-) | ||
| 29 | |||
| 30 | diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py | ||
| 31 | index e2d1846..c77702f 100644 | ||
| 32 | --- a/django/db/models/fields/__init__.py | ||
| 33 | +++ b/django/db/models/fields/__init__.py | ||
| 34 | @@ -26,7 +26,7 @@ from django.utils.dateparse import ( | ||
| 35 | ) | ||
| 36 | from django.utils.duration import duration_microseconds, duration_string | ||
| 37 | from django.utils.functional import Promise, cached_property | ||
| 38 | -from django.utils.ipv6 import clean_ipv6_address | ||
| 39 | +from django.utils.ipv6 import MAX_IPV6_ADDRESS_LENGTH, clean_ipv6_address | ||
| 40 | from django.utils.itercompat import is_iterable | ||
| 41 | from django.utils.text import capfirst | ||
| 42 | from django.utils.translation import gettext_lazy as _ | ||
| 43 | @@ -1904,7 +1904,7 @@ class GenericIPAddressField(Field): | ||
| 44 | self.default_validators, invalid_error_message = \ | ||
| 45 | validators.ip_address_validators(protocol, unpack_ipv4) | ||
| 46 | self.default_error_messages['invalid'] = invalid_error_message | ||
| 47 | - kwargs['max_length'] = 39 | ||
| 48 | + kwargs["max_length"] = MAX_IPV6_ADDRESS_LENGTH | ||
| 49 | super().__init__(verbose_name, name, *args, **kwargs) | ||
| 50 | |||
| 51 | def check(self, **kwargs): | ||
| 52 | @@ -1931,7 +1931,7 @@ class GenericIPAddressField(Field): | ||
| 53 | kwargs['unpack_ipv4'] = self.unpack_ipv4 | ||
| 54 | if self.protocol != "both": | ||
| 55 | kwargs['protocol'] = self.protocol | ||
| 56 | - if kwargs.get("max_length") == 39: | ||
| 57 | + if kwargs.get("max_length") == self.max_length: | ||
| 58 | del kwargs['max_length'] | ||
| 59 | return name, path, args, kwargs | ||
| 60 | |||
| 61 | diff --git a/django/forms/fields.py b/django/forms/fields.py | ||
| 62 | index f939338..b3156b9 100644 | ||
| 63 | --- a/django/forms/fields.py | ||
| 64 | +++ b/django/forms/fields.py | ||
| 65 | @@ -29,7 +29,7 @@ from django.forms.widgets import ( | ||
| 66 | from django.utils import formats | ||
| 67 | from django.utils.dateparse import parse_duration | ||
| 68 | from django.utils.duration import duration_string | ||
| 69 | -from django.utils.ipv6 import clean_ipv6_address | ||
| 70 | +from django.utils.ipv6 import MAX_IPV6_ADDRESS_LENGTH, clean_ipv6_address | ||
| 71 | from django.utils.translation import gettext_lazy as _, ngettext_lazy | ||
| 72 | |||
| 73 | __all__ = ( | ||
| 74 | @@ -1162,6 +1162,7 @@ class GenericIPAddressField(CharField): | ||
| 75 | def __init__(self, *, protocol='both', unpack_ipv4=False, **kwargs): | ||
| 76 | self.unpack_ipv4 = unpack_ipv4 | ||
| 77 | self.default_validators = validators.ip_address_validators(protocol, unpack_ipv4)[0] | ||
| 78 | + kwargs.setdefault("max_length", MAX_IPV6_ADDRESS_LENGTH) | ||
| 79 | super().__init__(**kwargs) | ||
| 80 | |||
| 81 | def to_python(self, value): | ||
| 82 | @@ -1169,7 +1170,9 @@ class GenericIPAddressField(CharField): | ||
| 83 | return '' | ||
| 84 | value = value.strip() | ||
| 85 | if value and ':' in value: | ||
| 86 | - return clean_ipv6_address(value, self.unpack_ipv4) | ||
| 87 | + return clean_ipv6_address( | ||
| 88 | + value, self.unpack_ipv4, max_length=self.max_length | ||
| 89 | + ) | ||
| 90 | return value | ||
| 91 | |||
| 92 | |||
| 93 | diff --git a/django/utils/ipv6.py b/django/utils/ipv6.py | ||
| 94 | index ddb8c80..aed7902 100644 | ||
| 95 | --- a/django/utils/ipv6.py | ||
| 96 | +++ b/django/utils/ipv6.py | ||
| 97 | @@ -3,9 +3,23 @@ import ipaddress | ||
| 98 | from django.core.exceptions import ValidationError | ||
| 99 | from django.utils.translation import gettext_lazy as _ | ||
| 100 | |||
| 101 | +MAX_IPV6_ADDRESS_LENGTH = 39 | ||
| 102 | |||
| 103 | -def clean_ipv6_address(ip_str, unpack_ipv4=False, | ||
| 104 | - error_message=_("This is not a valid IPv6 address.")): | ||
| 105 | + | ||
| 106 | +def _ipv6_address_from_str(ip_str, max_length=MAX_IPV6_ADDRESS_LENGTH): | ||
| 107 | + if len(ip_str) > max_length: | ||
| 108 | + raise ValueError( | ||
| 109 | + f"Unable to convert {ip_str} to an IPv6 address (value too long)." | ||
| 110 | + ) | ||
| 111 | + return ipaddress.IPv6Address(int(ipaddress.IPv6Address(ip_str))) | ||
| 112 | + | ||
| 113 | + | ||
| 114 | +def clean_ipv6_address( | ||
| 115 | + ip_str, | ||
| 116 | + unpack_ipv4=False, | ||
| 117 | + error_message=_("This is not a valid IPv6 address."), | ||
| 118 | + max_length=MAX_IPV6_ADDRESS_LENGTH, | ||
| 119 | + ): | ||
| 120 | """ | ||
| 121 | Clean an IPv6 address string. | ||
| 122 | |||
| 123 | @@ -23,7 +37,7 @@ def clean_ipv6_address(ip_str, unpack_ipv4=False, | ||
| 124 | Return a compressed IPv6 address or the same value. | ||
| 125 | """ | ||
| 126 | try: | ||
| 127 | - addr = ipaddress.IPv6Address(int(ipaddress.IPv6Address(ip_str))) | ||
| 128 | + addr = _ipv6_address_from_str(ip_str, max_length) | ||
| 129 | except ValueError: | ||
| 130 | raise ValidationError(error_message, code='invalid') | ||
| 131 | |||
| 132 | @@ -40,7 +54,7 @@ def is_valid_ipv6_address(ip_str): | ||
| 133 | Return whether or not the `ip_str` string is a valid IPv6 address. | ||
| 134 | """ | ||
| 135 | try: | ||
| 136 | - ipaddress.IPv6Address(ip_str) | ||
| 137 | + _ipv6_address_from_str(ip_str) | ||
| 138 | except ValueError: | ||
| 139 | return False | ||
| 140 | return True | ||
| 141 | diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt | ||
| 142 | index 3a888ef..688890a 100644 | ||
| 143 | --- a/docs/ref/forms/fields.txt | ||
| 144 | +++ b/docs/ref/forms/fields.txt | ||
| 145 | @@ -791,7 +791,7 @@ For each field, we describe the default widget used if you don't specify | ||
| 146 | * Empty value: ``''`` (an empty string) | ||
| 147 | * Normalizes to: A string. IPv6 addresses are normalized as described below. | ||
| 148 | * Validates that the given value is a valid IP address. | ||
| 149 | - * Error message keys: ``required``, ``invalid`` | ||
| 150 | + * Error message keys: ``required``, ``invalid``, ``max_length`` | ||
| 151 | |||
| 152 | The IPv6 address normalization follows :rfc:`4291#section-2.2` section 2.2, | ||
| 153 | including using the IPv4 format suggested in paragraph 3 of that section, like | ||
| 154 | @@ -799,7 +799,7 @@ For each field, we describe the default widget used if you don't specify | ||
| 155 | ``2001::1``, and ``::ffff:0a0a:0a0a`` to ``::ffff:10.10.10.10``. All characters | ||
| 156 | are converted to lowercase. | ||
| 157 | |||
| 158 | - Takes two optional arguments: | ||
| 159 | + Takes three optional arguments: | ||
| 160 | |||
| 161 | .. attribute:: protocol | ||
| 162 | |||
| 163 | @@ -814,6 +814,15 @@ For each field, we describe the default widget used if you don't specify | ||
| 164 | ``192.0.2.1``. Default is disabled. Can only be used | ||
| 165 | when ``protocol`` is set to ``'both'``. | ||
| 166 | |||
| 167 | + .. attribute:: max_length | ||
| 168 | + | ||
| 169 | + Defaults to 39, and behaves the same way as it does for | ||
| 170 | + :class:`CharField`. | ||
| 171 | + | ||
| 172 | + .. versionchanged:: 4.2.18 | ||
| 173 | + | ||
| 174 | + The default value for ``max_length`` was set to 39 characters. | ||
| 175 | + | ||
| 176 | ``MultipleChoiceField`` | ||
| 177 | ----------------------- | ||
| 178 | |||
| 179 | diff --git a/docs/releases/2.2.28.txt b/docs/releases/2.2.28.txt | ||
| 180 | index 7096d13..0e092f0 100644 | ||
| 181 | --- a/docs/releases/2.2.28.txt | ||
| 182 | +++ b/docs/releases/2.2.28.txt | ||
| 183 | @@ -105,3 +105,15 @@ CVE-2025-26699: Potential denial-of-service vulnerability in ``django.utils.text | ||
| 184 | The ``wrap()`` and :tfilter:`wordwrap` template filter were subject to a | ||
| 185 | potential denial-of-service attack when used with very long strings. | ||
| 186 | |||
| 187 | +CVE-2024-56374: Potential denial-of-service vulnerability in IPv6 validation | ||
| 188 | +============================================================================ | ||
| 189 | + | ||
| 190 | +Lack of upper bound limit enforcement in strings passed when performing IPv6 | ||
| 191 | +validation could lead to a potential denial-of-service attack. The undocumented | ||
| 192 | +and private functions ``clean_ipv6_address`` and ``is_valid_ipv6_address`` were | ||
| 193 | +vulnerable, as was the :class:`django.forms.GenericIPAddressField` form field, | ||
| 194 | +which has now been updated to define a ``max_length`` of 39 characters. | ||
| 195 | + | ||
| 196 | +The :class:`django.db.models.GenericIPAddressField` model field was not | ||
| 197 | +affected. | ||
| 198 | + | ||
| 199 | diff --git a/tests/forms_tests/field_tests/test_genericipaddressfield.py b/tests/forms_tests/field_tests/test_genericipaddressfield.py | ||
| 200 | index 97a83e3..4c79d78 100644 | ||
| 201 | --- a/tests/forms_tests/field_tests/test_genericipaddressfield.py | ||
| 202 | +++ b/tests/forms_tests/field_tests/test_genericipaddressfield.py | ||
| 203 | @@ -1,5 +1,6 @@ | ||
| 204 | from django.forms import GenericIPAddressField, ValidationError | ||
| 205 | from django.test import SimpleTestCase | ||
| 206 | +from django.utils.ipv6 import MAX_IPV6_ADDRESS_LENGTH | ||
| 207 | |||
| 208 | |||
| 209 | class GenericIPAddressFieldTest(SimpleTestCase): | ||
| 210 | @@ -89,6 +90,35 @@ class GenericIPAddressFieldTest(SimpleTestCase): | ||
| 211 | with self.assertRaisesMessage(ValidationError, "'This is not a valid IPv6 address.'"): | ||
| 212 | f.clean('1:2') | ||
| 213 | |||
| 214 | + def test_generic_ipaddress_max_length_custom(self): | ||
| 215 | + # Valid IPv4-mapped IPv6 address, len 45. | ||
| 216 | + addr = "0000:0000:0000:0000:0000:ffff:192.168.100.228" | ||
| 217 | + f = GenericIPAddressField(max_length=len(addr)) | ||
| 218 | + f.clean(addr) | ||
| 219 | + | ||
| 220 | + def test_generic_ipaddress_max_length_validation_error(self): | ||
| 221 | + # Valid IPv4-mapped IPv6 address, len 45. | ||
| 222 | + addr = "0000:0000:0000:0000:0000:ffff:192.168.100.228" | ||
| 223 | + | ||
| 224 | + cases = [ | ||
| 225 | + ({}, MAX_IPV6_ADDRESS_LENGTH), # Default value. | ||
| 226 | + ({"max_length": len(addr) - 1}, len(addr) - 1), | ||
| 227 | + ] | ||
| 228 | + for kwargs, max_length in cases: | ||
| 229 | + max_length_plus_one = max_length + 1 | ||
| 230 | + msg = ( | ||
| 231 | + f"Ensure this value has at most {max_length} characters (it has " | ||
| 232 | + f"{max_length_plus_one}).'" | ||
| 233 | + ) | ||
| 234 | + with self.subTest(max_length=max_length): | ||
| 235 | + f = GenericIPAddressField(**kwargs) | ||
| 236 | + with self.assertRaisesMessage(ValidationError, msg): | ||
| 237 | + f.clean("x" * max_length_plus_one) | ||
| 238 | + with self.assertRaisesMessage( | ||
| 239 | + ValidationError, "This is not a valid IPv6 address." | ||
| 240 | + ): | ||
| 241 | + f.clean(addr) | ||
| 242 | + | ||
| 243 | def test_generic_ipaddress_as_generic_not_required(self): | ||
| 244 | f = GenericIPAddressField(required=False) | ||
| 245 | self.assertEqual(f.clean(''), '') | ||
| 246 | @@ -103,7 +133,10 @@ class GenericIPAddressFieldTest(SimpleTestCase): | ||
| 247 | with self.assertRaisesMessage(ValidationError, "'Enter a valid IPv4 or IPv6 address.'"): | ||
| 248 | f.clean('256.125.1.5') | ||
| 249 | self.assertEqual(f.clean(' fe80::223:6cff:fe8a:2e8a '), 'fe80::223:6cff:fe8a:2e8a') | ||
| 250 | - self.assertEqual(f.clean(' 2a02::223:6cff:fe8a:2e8a '), '2a02::223:6cff:fe8a:2e8a') | ||
| 251 | + self.assertEqual( | ||
| 252 | + f.clean(" " * MAX_IPV6_ADDRESS_LENGTH + " 2a02::223:6cff:fe8a:2e8a "), | ||
| 253 | + "2a02::223:6cff:fe8a:2e8a", | ||
| 254 | + ) | ||
| 255 | with self.assertRaisesMessage(ValidationError, "'This is not a valid IPv6 address.'"): | ||
| 256 | f.clean('12345:2:3:4') | ||
| 257 | with self.assertRaisesMessage(ValidationError, "'This is not a valid IPv6 address.'"): | ||
| 258 | diff --git a/tests/utils_tests/test_ipv6.py b/tests/utils_tests/test_ipv6.py | ||
| 259 | index 4e434f3..1ac6763 100644 | ||
| 260 | --- a/tests/utils_tests/test_ipv6.py | ||
| 261 | +++ b/tests/utils_tests/test_ipv6.py | ||
| 262 | @@ -1,9 +1,17 @@ | ||
| 263 | -import unittest | ||
| 264 | +import traceback | ||
| 265 | +from io import StringIO | ||
| 266 | |||
| 267 | -from django.utils.ipv6 import clean_ipv6_address, is_valid_ipv6_address | ||
| 268 | +from django.core.exceptions import ValidationError | ||
| 269 | +from django.test import SimpleTestCase | ||
| 270 | +from django.utils.ipv6 import ( | ||
| 271 | + MAX_IPV6_ADDRESS_LENGTH, | ||
| 272 | + clean_ipv6_address, | ||
| 273 | + is_valid_ipv6_address, | ||
| 274 | +) | ||
| 275 | +from django.utils.version import PY310 | ||
| 276 | |||
| 277 | |||
| 278 | -class TestUtilsIPv6(unittest.TestCase): | ||
| 279 | +class TestUtilsIPv6(SimpleTestCase): | ||
| 280 | |||
| 281 | def test_validates_correct_plain_address(self): | ||
| 282 | self.assertTrue(is_valid_ipv6_address('fe80::223:6cff:fe8a:2e8a')) | ||
| 283 | @@ -55,3 +63,29 @@ class TestUtilsIPv6(unittest.TestCase): | ||
| 284 | self.assertEqual(clean_ipv6_address('::ffff:0a0a:0a0a', unpack_ipv4=True), '10.10.10.10') | ||
| 285 | self.assertEqual(clean_ipv6_address('::ffff:1234:1234', unpack_ipv4=True), '18.52.18.52') | ||
| 286 | self.assertEqual(clean_ipv6_address('::ffff:18.52.18.52', unpack_ipv4=True), '18.52.18.52') | ||
| 287 | + | ||
| 288 | + def test_address_too_long(self): | ||
| 289 | + addresses = [ | ||
| 290 | + "0000:0000:0000:0000:0000:ffff:192.168.100.228", # IPv4-mapped IPv6 address | ||
| 291 | + "0000:0000:0000:0000:0000:ffff:192.168.100.228%123456", # % scope/zone | ||
| 292 | + "fe80::223:6cff:fe8a:2e8a:1234:5678:00000", # MAX_IPV6_ADDRESS_LENGTH + 1 | ||
| 293 | + ] | ||
| 294 | + msg = "This is the error message." | ||
| 295 | + value_error_msg = "Unable to convert %s to an IPv6 address (value too long)." | ||
| 296 | + for addr in addresses: | ||
| 297 | + with self.subTest(addr=addr): | ||
| 298 | + self.assertGreater(len(addr), MAX_IPV6_ADDRESS_LENGTH) | ||
| 299 | + self.assertEqual(is_valid_ipv6_address(addr), False) | ||
| 300 | + with self.assertRaisesMessage(ValidationError, msg) as ctx: | ||
| 301 | + clean_ipv6_address(addr, error_message=msg) | ||
| 302 | + exception_traceback = StringIO() | ||
| 303 | + if PY310: | ||
| 304 | + traceback.print_exception(ctx.exception, file=exception_traceback) | ||
| 305 | + else: | ||
| 306 | + traceback.print_exception( | ||
| 307 | + type(ctx.exception), | ||
| 308 | + value=ctx.exception, | ||
| 309 | + tb=ctx.exception.__traceback__, | ||
| 310 | + file=exception_traceback, | ||
| 311 | + ) | ||
| 312 | + self.assertIn(value_error_msg % addr, exception_traceback.getvalue()) | ||
| 313 | -- | ||
| 314 | 2.40.0 | ||
| 315 | |||
diff --git a/meta-python/recipes-devtools/python/python3-django_2.2.28.bb b/meta-python/recipes-devtools/python/python3-django_2.2.28.bb index 24eee95f03..f4b8da69b5 100644 --- a/meta-python/recipes-devtools/python/python3-django_2.2.28.bb +++ b/meta-python/recipes-devtools/python/python3-django_2.2.28.bb | |||
| @@ -26,6 +26,7 @@ SRC_URI += "file://CVE-2023-31047.patch \ | |||
| 26 | file://CVE-2024-53907.patch \ | 26 | file://CVE-2024-53907.patch \ |
| 27 | file://CVE-2024-27351.patch \ | 27 | file://CVE-2024-27351.patch \ |
| 28 | file://CVE-2025-26699.patch \ | 28 | file://CVE-2025-26699.patch \ |
| 29 | file://CVE-2024-56374.patch \ | ||
| 29 | " | 30 | " |
| 30 | 31 | ||
| 31 | SRC_URI[sha256sum] = "0200b657afbf1bc08003845ddda053c7641b9b24951e52acd51f6abda33a7413" | 32 | SRC_URI[sha256sum] = "0200b657afbf1bc08003845ddda053c7641b9b24951e52acd51f6abda33a7413" |
diff --git a/meta-python/recipes-devtools/python/python3-django_3.2.25.bb b/meta-python/recipes-devtools/python/python3-django_3.2.25.bb index fb6cb97710..452f2f87a6 100644 --- a/meta-python/recipes-devtools/python/python3-django_3.2.25.bb +++ b/meta-python/recipes-devtools/python/python3-django_3.2.25.bb | |||
| @@ -8,6 +8,7 @@ RDEPENDS:${PN} += "\ | |||
| 8 | " | 8 | " |
| 9 | SRC_URI += "\ | 9 | SRC_URI += "\ |
| 10 | file://CVE-2025-26699.patch \ | 10 | file://CVE-2025-26699.patch \ |
| 11 | file://CVE-2024-56374.patch \ | ||
| 11 | " | 12 | " |
| 12 | 13 | ||
| 13 | # Set DEFAULT_PREFERENCE so that the LTS version of django is built by | 14 | # Set DEFAULT_PREFERENCE so that the LTS version of django is built by |
