python3-django: fix CVE-2026-15337

This patch applies the upstream fix as referenced in [2],
using the commit shown in [1].

[1] https://github.com/django/django/commit/c72a5dbb64d0777f3f471f1be94e8b2ca91e0959
[2] https://nvd.nist.gov/vuln/detail/CVE-2026-15337

Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
Signed-off-by: Anuj Mittal <anuj.mittal@oss.qualcomm.com>
This commit is contained in:
Darsh Kelaiya
2026-09-01 10:17:59 +05:30
committed by Anuj Mittal
parent 3bc6095e25
commit e1fadab9dd
2 changed files with 164 additions and 0 deletions
@@ -0,0 +1,163 @@
From 8c63ca97dd33e3cb7e4a6f78c5f616f2e45ecaf1 Mon Sep 17 00:00:00 2001
From: Natalia <124304+nessita@users.noreply.github.com>
Date: Fri, 10 Jul 2026 18:30:21 -0300
Subject: [PATCH] [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 27137e655e442e81095f1f8f77ff3870d9fdf169 from main.
CVE: CVE-2026-15337
Upstream-Status: Backport [https://github.com/django/django/commit/c72a5dbb64d0777f3f471f1be94e8b2ca91e0959]
Backport Changes:
- Dropped the docs/release file as current version
is 5.0.14 for Scarthgap.
(cherry picked from commit c72a5dbb64d0777f3f471f1be94e8b2ca91e0959)
Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
---
django/test/signals.py | 2 +-
django/utils/translation/trans_real.py | 27 ++++++++++++++++++--------
docs/ref/utils.txt | 3 +++
tests/i18n/tests.py | 24 ++++++++++++++++++++++-
4 files changed, 46 insertions(+), 10 deletions(-)
diff --git a/django/test/signals.py b/django/test/signals.py
index c16f4aa5ee..51ff9c3d98 100644
--- a/django/test/signals.py
+++ b/django/test/signals.py
@@ -152,7 +152,7 @@ def language_changed(*, setting, **kwargs):
from django.utils.translation import trans_real
trans_real._translations = {}
- trans_real.check_for_language.cache_clear()
+ trans_real.translation_catalog_exists.cache_clear()
@receiver(setting_changed)
diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py
index 1c42330451..67937a6470 100644
--- a/django/utils/translation/trans_real.py
+++ b/django/utils/translation/trans_real.py
@@ -31,9 +31,10 @@ _default = None
# magic gettext number to separate context from message
CONTEXT_SEPARATOR = "\x04"
-# Maximum number of characters that will be parsed from the Accept-Language
-# header or cookie to prevent possible denial of service or memory exhaustion
-# attacks. About 10x longer than the longest value shown on MDNs
+# Maximum length of a language code that will be processed, to prevent possible
+# denial of service or memory exhaustion attacks. Language codes are taken from
+# the Accept-Language header, the language cookie, the URL path prefix, or the
+# set_language() view. 500 is about 10x the longest value shown on MDN's
# Accept-Language page.
LANGUAGE_CODE_MAX_LENGTH = 500
@@ -65,7 +66,7 @@ def reset_cache(*, setting, **kwargs):
languages should no longer be accepted.
"""
if setting in ("LANGUAGES", "LANGUAGE_CODE"):
- check_for_language.cache_clear()
+ translation_catalog_exists.cache_clear()
get_languages.cache_clear()
get_supported_language_variant.cache_clear()
@@ -461,19 +462,29 @@ def all_locale_paths():
return [globalpath, *settings.LOCALE_PATHS, *app_paths]
-@functools.lru_cache(maxsize=1000)
def check_for_language(lang_code):
"""
Check whether there is a global language file for the given language
code. This is used to decide whether a user-provided language is
available.
- 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
+ Reject over-length codes before the cached lookup so that oversized,
+ attacker-controlled values are not retained as cache keys.
+ """
+ if lang_code is None or len(lang_code) > LANGUAGE_CODE_MAX_LENGTH:
+ return False
+ return translation_catalog_exists(lang_code)
+
+
+@functools.lru_cache(maxsize=1000)
+def translation_catalog_exists(lang_code):
+ """Return whether a translation catalog exists for the given language code.
+
+ lru_cache should have a maxsize to prevent memory exhaustion attacks. See:
<https://www.djangoproject.com/weblog/2007/oct/26/security-fix/>.
"""
# First, a quick check to make sure lang_code is well-formed (#21458)
- if lang_code is None or not language_code_re.search(lang_code):
+ if not language_code_re.search(lang_code):
return False
return any(
gettext_module.find("django", path, [to_locale(lang_code)]) is not None
diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt
index 1d0178a263..1f44b4eb85 100644
--- a/docs/ref/utils.txt
+++ b/docs/ref/utils.txt
@@ -1082,6 +1082,9 @@ For a complete discussion on the usage of the following see the
code (e.g. 'fr', 'pt_BR'). This is used to decide whether a user-provided
language is available.
+ ``lang_code`` has a maximum accepted length of 500 characters. ``False``
+ is returned if it exceeds this limit, before any language-file lookup.
+
.. function:: get_language()
Returns the currently selected language code. Returns ``None`` if
diff --git a/tests/i18n/tests.py b/tests/i18n/tests.py
index f74e33bf79..b83c9d6a68 100644
--- a/tests/i18n/tests.py
+++ b/tests/i18n/tests.py
@@ -58,7 +58,10 @@ 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 django.utils.translation.trans_real import (
+ LANGUAGE_CODE_MAX_LENGTH,
+ translation_catalog_exists,
+)
from .forms import CompanyForm, I18nForm, SelectDateForm
from .models import Company, TestModel
@@ -1995,6 +1998,25 @@ class CountrySpecificLanguageTests(SimpleTestCase):
self.assertFalse(check_for_language("tr-TR.UTF8"))
self.assertFalse(check_for_language("de-DE.utf-8"))
+ def test_check_for_language_lang_code_max_length(self):
+ self.addCleanup(translation_catalog_exists.cache_clear)
+
+ # Overly long codes are rejected before the cached lookup, so they are
+ # not retained as cache keys, potentially consuming too much memory.
+ # Codes at the maximum length can reach the cached lookup.
+ for length, cache_size in [
+ (LANGUAGE_CODE_MAX_LENGTH - 1, 1),
+ (LANGUAGE_CODE_MAX_LENGTH, 1),
+ (LANGUAGE_CODE_MAX_LENGTH + 1, 0),
+ ]:
+ translation_catalog_exists.cache_clear()
+ with self.subTest(length=length):
+ self.assertIs(check_for_language("a" * length), False)
+ self.assertEqual(
+ translation_catalog_exists.cache_info().currsize,
+ cache_size,
+ )
+
def test_check_for_language_null(self):
self.assertIs(trans_null.check_for_language("en"), True)
--
2.44.4