Visitar URL original
[5.2.x] Fixed CVE-2026-77050 -- Prevented memory exhaustion in get_su… · django/django@02a69e3 · GitHub
Skip to content

Commit 02a69e3

Browse files
committed
[5.2.x] Fixed CVE-2026-77050 -- Prevented memory exhaustion in get_supported_language_variant().
The fix for CVE-2024-39614 limited the length of language codes processed by get_supported_language_variant(), but the full language code was retained as an lru_cache key before the length check. Moved caching after language code truncation and validation to prevent excessively long language codes from being retained in memory. Thank you to Gleb Lizunov for the report and Jacob Walls for the review. Backport of c88b304 from main.
1 parent f59aef2 commit 02a69e3

3 files changed

Lines changed: 88 additions & 37 deletions

File tree

‎django/utils/translation/trans_real.py‎

Lines changed: 35 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def reset_cache(*, setting, **kwargs):
6868
if setting in ("LANGUAGES", "LANGUAGE_CODE"):
6969
translation_catalog_exists.cache_clear()
7070
get_languages.cache_clear()
71-
get_supported_language_variant.cache_clear()
71+
_get_supported_language_variant.cache_clear()
7272

7373

7474
class TranslationCatalog:
@@ -499,6 +499,31 @@ def get_languages():
499499

500500

501501
@functools.lru_cache(maxsize=1000)
502+
def _get_supported_language_variant(lang_code, strict=False):
503+
# If 'zh-hant-tw' is not supported, try special fallback or subsequent
504+
# language codes i.e. 'zh-hant' and 'zh'.
505+
possible_lang_codes = [lang_code]
506+
try:
507+
possible_lang_codes.extend(LANG_INFO[lang_code]["fallback"])
508+
except KeyError:
509+
pass
510+
i = None
511+
while (i := lang_code.rfind("-", 0, i)) > -1:
512+
possible_lang_codes.append(lang_code[:i])
513+
generic_lang_code = possible_lang_codes[-1]
514+
supported_lang_codes = get_languages()
515+
516+
for code in possible_lang_codes:
517+
if code.lower() in supported_lang_codes and check_for_language(code):
518+
return code
519+
if not strict:
520+
# if fr-fr is not supported, try fr-ca.
521+
for supported_code in supported_lang_codes:
522+
if supported_code.startswith(generic_lang_code + "-"):
523+
return supported_code
524+
raise LookupError(lang_code)
525+
526+
502527
def get_supported_language_variant(lang_code, strict=False):
503528
"""
504529
Return the language code that's listed in supported languages, possibly
@@ -509,44 +534,17 @@ def get_supported_language_variant(lang_code, strict=False):
509534
510535
The language code is truncated to a maximum length to avoid potential
511536
denial of service attacks.
512-
513-
lru_cache should have a maxsize to prevent from memory exhaustion attacks,
514-
as the provided language codes are taken from the HTTP request. See also
515-
<https://www.djangoproject.com/weblog/2007/oct/26/security-fix/>.
516537
"""
517538
if lang_code:
518-
# Truncate the language code to a maximum length to avoid potential
519-
# denial of service attacks.
520-
if len(lang_code) > LANGUAGE_CODE_MAX_LENGTH:
521-
if (
522-
not strict
523-
and (index := lang_code.rfind("-", 0, LANGUAGE_CODE_MAX_LENGTH)) > 0
524-
):
525-
# There is a generic variant under the maximum length accepted length.
526-
lang_code = lang_code[:index]
527-
else:
528-
raise LookupError(lang_code)
529-
# If 'zh-hant-tw' is not supported, try special fallback or subsequent
530-
# language codes i.e. 'zh-hant' and 'zh'.
531-
possible_lang_codes = [lang_code]
532-
try:
533-
possible_lang_codes.extend(LANG_INFO[lang_code]["fallback"])
534-
except KeyError:
535-
pass
536-
i = None
537-
while (i := lang_code.rfind("-", 0, i)) > -1:
538-
possible_lang_codes.append(lang_code[:i])
539-
generic_lang_code = possible_lang_codes[-1]
540-
supported_lang_codes = get_languages()
541-
542-
for code in possible_lang_codes:
543-
if code.lower() in supported_lang_codes and check_for_language(code):
544-
return code
545-
if not strict:
546-
# if fr-fr is not supported, try fr-ca.
547-
for supported_code in supported_lang_codes:
548-
if supported_code.startswith(generic_lang_code + "-"):
549-
return supported_code
539+
if len(lang_code) <= LANGUAGE_CODE_MAX_LENGTH:
540+
return _get_supported_language_variant(lang_code, strict=strict)
541+
if (
542+
not strict
543+
and (index := lang_code.rfind("-", 0, LANGUAGE_CODE_MAX_LENGTH)) > 0
544+
):
545+
# There is a generic variant under the maximum accepted length.
546+
lang_code = lang_code[:index]
547+
return _get_supported_language_variant(lang_code, strict=strict)
550548
raise LookupError(lang_code)
551549

552550

‎docs/releases/5.2.18.txt‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,20 @@ Django 5.2.18 fixes one security issue with severity "low", three security
88
issues with severity "moderate", and provides a fix for an insufficient
99
security mitigation in 5.2.17. It also fixes one data loss issue in 4.0.
1010

11+
CVE-2026-77050: Potential denial-of-service vulnerability in ``get_supported_language_variant()``
12+
=================================================================================================
13+
14+
:func:`~django.utils.translation.get_supported_language_variant` was subject to
15+
a potential denial-of-service attack when processing many distinct, very long
16+
language codes. Language codes were used as keys in an in-memory cache before
17+
their length was limited, potentially consuming excessive process memory.
18+
19+
To mitigate this vulnerability, language codes longer than 500 characters are
20+
now rejected or truncated before the cached lookup.
21+
22+
This issue has severity "low" according to the :ref:`Django security policy
23+
<severity-levels>`.
24+
1125
Bugfixes
1226
========
1327

‎tests/i18n/tests.py‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1730,6 +1730,45 @@ def test_get_supported_language_variant_real(self):
17301730
g("en-" * 167, strict=True)
17311731
self.assertEqual(g("en-" * 30000), "en") # catastrophic test
17321732

1733+
def test_get_supported_language_variant_strict_long_lang_code_not_cached(self):
1734+
g = trans_real.get_supported_language_variant
1735+
g_cached = trans_real._get_supported_language_variant
1736+
self.addCleanup(g_cached.cache_clear)
1737+
1738+
for lang_code in (
1739+
"e" * 501,
1740+
"en-" * 167, # Length 501 which is LANGUAGE_CODE_MAX_LENGTH + 1.
1741+
"en-" * 30000,
1742+
):
1743+
g_cached.cache_clear()
1744+
with self.subTest(length=len(lang_code)):
1745+
with self.assertRaises(LookupError):
1746+
g(lang_code, strict=True)
1747+
1748+
cache_info = g_cached.cache_info()
1749+
self.assertEqual(cache_info.currsize, 0)
1750+
self.assertEqual(cache_info.misses, 0)
1751+
1752+
def test_get_supported_language_variant_truncated_cache_key(self):
1753+
g = trans_real.get_supported_language_variant
1754+
g_cached = trans_real._get_supported_language_variant
1755+
self.addCleanup(g_cached.cache_clear)
1756+
g_cached.cache_clear()
1757+
1758+
long_code = "en-" * 167 # Length 501.
1759+
truncated_code = "en-" * 165 + "en" # Length 497.
1760+
catastrophic_long_code = "en-" * 3000
1761+
1762+
for hits, code in enumerate(
1763+
[long_code, truncated_code, catastrophic_long_code]
1764+
):
1765+
with self.subTest(code_length=len(code)):
1766+
self.assertEqual(g(code), "en")
1767+
cache_info = g_cached.cache_info()
1768+
self.assertEqual(cache_info.currsize, 1)
1769+
self.assertEqual(cache_info.hits, hits)
1770+
self.assertEqual(cache_info.misses, 1)
1771+
17331772
def test_get_supported_language_variant_null(self):
17341773
g = trans_null.get_supported_language_variant
17351774
self.assertEqual(g(settings.LANGUAGE_CODE), settings.LANGUAGE_CODE)

0 commit comments

Comments
 (0)