티스토리 수익 글 보기

티스토리 수익 글 보기

[5.2.x] Fixed CVE-2026-15337 — Mitigated potential DoS in check_for_… · django/django@c72a5db · GitHub
Skip to content

Commit c72a5db

Browse files
committed
[5.2.x] Fixed CVE-2026-15337 — Mitigated potential DoS in check_for_language().
Language codes longer than 500 characters are now rejected before the cached lookup, so they are no longer retained as cache keys consuming memory from each process. Thanks Jaeyoung Jang for the report, and Sarah Boyce for reviews. Backport of 27137e6 from main.
1 parent 115ffd0 commit c72a5db

5 files changed

Lines changed: 65 additions & 10 deletions

File tree

django/test/signals.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ def language_changed(*, setting, **kwargs):
151151
from django.utils.translation import trans_real
152152

153153
trans_real._translations = {}
154-
trans_real.check_for_language.cache_clear()
154+
trans_real.translation_catalog_exists.cache_clear()
155155

156156

157157
@receiver(setting_changed)

django/utils/translation/trans_real.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,10 @@
3131
# magic gettext number to separate context from message
3232
CONTEXT_SEPARATOR = "\x04"
3333

34-
# Maximum number of characters that will be parsed from the Accept-Language
35-
# header or cookie to prevent possible denial of service or memory exhaustion
36-
# attacks. About 10x longer than the longest value shown on MDN’s
34+
# Maximum length of a language code that will be processed, to prevent possible
35+
# denial of service or memory exhaustion attacks. Language codes are taken from
36+
# the Accept-Language header, the language cookie, the URL path prefix, or the
37+
# set_language() view. 500 is about 10x the longest value shown on MDN's
3738
# Accept-Language page.
3839
LANGUAGE_CODE_MAX_LENGTH = 500
3940

@@ -65,7 +66,7 @@ def reset_cache(*, setting, **kwargs):
6566
languages should no longer be accepted.
6667
"""
6768
if setting in ("LANGUAGES", "LANGUAGE_CODE"):
68-
check_for_language.cache_clear()
69+
translation_catalog_exists.cache_clear()
6970
get_languages.cache_clear()
7071
get_supported_language_variant.cache_clear()
7172

@@ -458,19 +459,29 @@ def all_locale_paths():
458459
return [globalpath, *settings.LOCALE_PATHS, *app_paths]
459460

460461

461-
@functools.lru_cache(maxsize=1000)
462462
def check_for_language(lang_code):
463463
"""
464464
Check whether there is a global language file for the given language
465465
code. This is used to decide whether a user-provided language is
466466
available.
467467
468-
lru_cache should have a maxsize to prevent from memory exhaustion attacks,
469-
as the provided language codes are taken from the HTTP request. See also
468+
Reject over-length codes before the cached lookup so that oversized,
469+
attacker-controlled values are not retained as cache keys.
470+
"""
471+
if lang_code is None or len(lang_code) > LANGUAGE_CODE_MAX_LENGTH:
472+
return False
473+
return translation_catalog_exists(lang_code)
474+
475+
476+
@functools.lru_cache(maxsize=1000)
477+
def translation_catalog_exists(lang_code):
478+
"""Return whether a translation catalog exists for the given language code.
479+
480+
lru_cache should have a maxsize to prevent memory exhaustion attacks. See:
470481
<https://www.djangoproject.com/weblog/2007/oct/26/security-fix/>.
471482
"""
472483
# First, a quick check to make sure lang_code is well-formed (#21458)
473-
if lang_code is None or not language_code_re.search(lang_code):
484+
if not language_code_re.search(lang_code):
474485
return False
475486
return any(
476487
gettext_module.find("django", path, [to_locale(lang_code)]) is not None

docs/ref/utils.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1135,6 +1135,9 @@ For a complete discussion on the usage of the following see the
11351135
code (e.g. 'fr', 'pt_BR'). This is used to decide whether a user-provided
11361136
language is available.
11371137

1138+
``lang_code`` has a maximum accepted length of 500 characters. ``False``
1139+
is returned if it exceeds this limit, before any language-file lookup.
1140+
11381141
.. function:: get_language()
11391142

11401143
Returns the currently selected language code. Returns ``None`` if

docs/releases/5.2.17.txt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,22 @@ untrusted input, and on further security considerations, see
3636

3737
This issue has severity "high" according to the :ref:`Django security policy
3838
<severity-levels>`.
39+
40+
CVE-2026-15337: Potential denial-of-service vulnerability in ``check_for_language()``
41+
=====================================================================================
42+
43+
:func:`~django.utils.translation.check_for_language` was subject to a potential
44+
denial-of-service attack when checking many distinct, very long language codes.
45+
Each code was used as a key in an in-memory cache, consuming process memory.
46+
47+
The ``language`` value reaches this function through the
48+
:func:`django.views.i18n.set_language` view (not active by default) from POST
49+
data. Since request data is limited by :setting:`DATA_UPLOAD_MAX_MEMORY_SIZE`
50+
and the cache is configured to store a maximum number of entries, the memory
51+
that could be consumed was bounded.
52+
53+
To mitigate this vulnerability, language codes longer than 500 characters are
54+
now rejected before the cached lookup.
55+
56+
This issue has severity "low" according to the :ref:`Django security policy
57+
<severity-levels>`.

tests/i18n/tests.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@
5959
translation_file_changed,
6060
watch_for_translation_changes,
6161
)
62-
from django.utils.translation.trans_real import LANGUAGE_CODE_MAX_LENGTH
62+
from django.utils.translation.trans_real import (
63+
LANGUAGE_CODE_MAX_LENGTH,
64+
translation_catalog_exists,
65+
)
6366

6467
from .forms import CompanyForm, I18nForm, SelectDateForm
6568
from .models import Company, TestModel
@@ -2036,6 +2039,25 @@ def test_check_for_language(self):
20362039
self.assertFalse(check_for_language("tr-TR.UTF8"))
20372040
self.assertFalse(check_for_language("de-DE.utf-8"))
20382041

2042+
def test_check_for_language_lang_code_max_length(self):
2043+
self.addCleanup(translation_catalog_exists.cache_clear)
2044+
2045+
# Overly long codes are rejected before the cached lookup, so they are
2046+
# not retained as cache keys, potentially consuming too much memory.
2047+
# Codes at the maximum length can reach the cached lookup.
2048+
for length, cache_size in [
2049+
(LANGUAGE_CODE_MAX_LENGTH - 1, 1),
2050+
(LANGUAGE_CODE_MAX_LENGTH, 1),
2051+
(LANGUAGE_CODE_MAX_LENGTH + 1, 0),
2052+
]:
2053+
translation_catalog_exists.cache_clear()
2054+
with self.subTest(length=length):
2055+
self.assertIs(check_for_language("a" * length), False)
2056+
self.assertEqual(
2057+
translation_catalog_exists.cache_info().currsize,
2058+
cache_size,
2059+
)
2060+
20392061
def test_check_for_language_null(self):
20402062
self.assertIs(trans_null.check_for_language("en"), True)
20412063

0 commit comments

Comments
 (0)