File: 0017-CVE-2024-39614-1.patch

package info (click to toggle)
python-django 3%3A3.2.19-1%2Bdeb12u2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm-proposed-updates
  • size: 56,696 kB
  • sloc: python: 264,418; javascript: 18,362; xml: 193; makefile: 178; sh: 43
file content (127 lines) | stat: -rw-r--r-- 5,862 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
commit 9e9792228a6bb5d6402a5d645bc3be4cf364aefb
Author: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com>
Date:   Wed Jun 26 12:11:54 2024 +0200

    Fixed CVE-2024-39614 -- Mitigated potential DoS in get_supported_language_variant().
    
    Language codes are now parsed with a maximum length limit of 500 chars.
    
    Thanks to MProgrammer for the report.

diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py
index b262a5000..92442185f 100644
--- a/django/utils/translation/trans_real.py
+++ b/django/utils/translation/trans_real.py
@@ -31,9 +31,10 @@ _default = None
 CONTEXT_SEPARATOR = "\x04"
 
 # Maximum number of characters that will be parsed from the Accept-Language
-# header to prevent possible denial of service or memory exhaustion attacks.
-# About 10x longer than the longest value shown on MDN’s Accept-Language page.
-ACCEPT_LANGUAGE_HEADER_MAX_LENGTH = 500
+# header or cookie to prevent possible denial of service or memory exhaustion
+# attacks. About 10x longer than the longest value shown on MDN’s
+# Accept-Language page.
+LANGUAGE_CODE_MAX_LENGTH = 500
 
 # Format of Accept-Language header values. From RFC 2616, section 14.4 and 3.9
 # and RFC 3066, section 2.1
@@ -474,11 +475,25 @@ def get_supported_language_variant(lang_code, strict=False):
     If `strict` is False (the default), look for a country-specific variant
     when neither the language code nor its generic variant is found.
 
+    The language code is truncated to a maximum length to avoid potential
+    denial of service attacks.
+
     lru_cache should have a maxsize to prevent from memory exhaustion attacks,
     as the provided language codes are taken from the HTTP request. See also
     <https://www.djangoproject.com/weblog/2007/oct/26/security-fix/>.
     """
     if lang_code:
+        # Truncate the language code to a maximum length to avoid potential
+        # denial of service attacks.
+        if len(lang_code) > LANGUAGE_CODE_MAX_LENGTH:
+            if (
+                not strict
+                and (index := lang_code.rfind("-", 0, LANGUAGE_CODE_MAX_LENGTH)) > 0
+            ):
+                # There is a generic variant under the maximum length accepted length.
+                lang_code = lang_code[:index]
+            else:
+                raise ValueError("'lang_code' exceeds the maximum accepted length")
         # If 'fr-ca' is not supported, try special fallback or language-only 'fr'.
         possible_lang_codes = [lang_code]
         try:
@@ -595,13 +610,13 @@ def parse_accept_lang_header(lang_string):
     functools.lru_cache() to avoid repetitive parsing of common header values.
     """
     # If the header value doesn't exceed the maximum allowed length, parse it.
-    if len(lang_string) <= ACCEPT_LANGUAGE_HEADER_MAX_LENGTH:
+    if len(lang_string) <= LANGUAGE_CODE_MAX_LENGTH:
         return _parse_accept_lang_header(lang_string)
 
     # If there is at least one comma in the value, parse up to the last comma
     # before the max length, skipping any truncated parts at the end of the
     # header value.
-    index = lang_string.rfind(",", 0, ACCEPT_LANGUAGE_HEADER_MAX_LENGTH)
+    index = lang_string.rfind(",", 0, LANGUAGE_CODE_MAX_LENGTH)
     if index > 0:
         return _parse_accept_lang_header(lang_string[:index])
 
diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt
index ce3a4cba0..00a3f5e79 100644
--- a/docs/ref/utils.txt
+++ b/docs/ref/utils.txt
@@ -1129,6 +1129,11 @@ For a complete discussion on the usage of the following see the
     ``lang_code`` is ``'es-ar'`` and ``'es'`` is in :setting:`LANGUAGES` but
     ``'es-ar'`` isn't.
 
+    ``lang_code`` has a maximum accepted length of 500 characters. A
+    :exc:`ValueError` is raised if ``lang_code`` exceeds this limit and
+    ``strict`` is ``True``, or if there is no generic variant and ``strict``
+    is ``False``.
+
     If ``strict`` is ``False`` (the default), a country-specific variant may
     be returned when neither the language code nor its generic variant is found.
     For example, if only ``'es-co'`` is in :setting:`LANGUAGES`, that's
@@ -1137,6 +1142,11 @@ For a complete discussion on the usage of the following see the
 
     Raises :exc:`LookupError` if nothing is found.
 
+    .. versionchanged:: 4.2.14
+
+        In older versions, ``lang_code`` values over 500 characters were
+        processed without raising a :exc:`ValueError`.
+
 .. function:: to_locale(language)
 
     Turns a language name (en-us) into a locale name (en_US).
diff --git a/tests/i18n/tests.py b/tests/i18n/tests.py
index 41ec63da9..793bd3db8 100644
--- a/tests/i18n/tests.py
+++ b/tests/i18n/tests.py
@@ -40,6 +40,7 @@ from django.utils.translation import (
 from django.utils.translation.reloader import (
     translation_file_changed, watch_for_translation_changes,
 )
+from django.utils.translation.trans_real import LANGUAGE_CODE_MAX_LENGTH
 
 from .forms import CompanyForm, I18nForm, SelectDateForm
 from .models import Company, TestModel
@@ -1532,6 +1533,16 @@ class MiscTests(SimpleTestCase):
             g('xyz')
         with self.assertRaises(LookupError):
             g('xy-zz')
+        msg = "'lang_code' exceeds the maximum accepted length"
+        with self.assertRaises(LookupError):
+            g("x" * LANGUAGE_CODE_MAX_LENGTH)
+        with self.assertRaisesMessage(ValueError, msg):
+            g("x" * (LANGUAGE_CODE_MAX_LENGTH + 1))
+        # 167 * 3 = 501 which is LANGUAGE_CODE_MAX_LENGTH + 1.
+        self.assertEqual(g("en-" * 167), "en")
+        with self.assertRaisesMessage(ValueError, msg):
+            g("en-" * 167, strict=True)
+        self.assertEqual(g("en-" * 30000), "en")  # catastrophic test
 
     def test_get_supported_language_variant_null(self):
         g = trans_null.get_supported_language_variant