1
0
mirror of https://github.com/django/django.git synced 2024-12-23 09:36:06 +00:00

[4.2.x] 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.
This commit is contained in:
Sarah Boyce 2024-06-26 12:11:54 +02:00 committed by Natalia
parent 2b00edc015
commit 17358fb35f
4 changed files with 56 additions and 5 deletions

View File

@ -31,9 +31,10 @@ _default = None
CONTEXT_SEPARATOR = "\x04" CONTEXT_SEPARATOR = "\x04"
# Maximum number of characters that will be parsed from the Accept-Language # Maximum number of characters that will be parsed from the Accept-Language
# header to prevent possible denial of service or memory exhaustion attacks. # header or cookie to prevent possible denial of service or memory exhaustion
# About 10x longer than the longest value shown on MDNs Accept-Language page. # attacks. About 10x longer than the longest value shown on MDNs
ACCEPT_LANGUAGE_HEADER_MAX_LENGTH = 500 # Accept-Language page.
LANGUAGE_CODE_MAX_LENGTH = 500
# Format of Accept-Language header values. From RFC 9110 Sections 12.4.2 and # Format of Accept-Language header values. From RFC 9110 Sections 12.4.2 and
# 12.5.4, and RFC 5646 Section 2.1. # 12.5.4, and RFC 5646 Section 2.1.
@ -497,11 +498,25 @@ def get_supported_language_variant(lang_code, strict=False):
If `strict` is False (the default), look for a country-specific variant If `strict` is False (the default), look for a country-specific variant
when neither the language code nor its generic variant is found. 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, 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 as the provided language codes are taken from the HTTP request. See also
<https://www.djangoproject.com/weblog/2007/oct/26/security-fix/>. <https://www.djangoproject.com/weblog/2007/oct/26/security-fix/>.
""" """
if lang_code: 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 'zh-hant-tw' is not supported, try special fallback or subsequent # If 'zh-hant-tw' is not supported, try special fallback or subsequent
# language codes i.e. 'zh-hant' and 'zh'. # language codes i.e. 'zh-hant' and 'zh'.
possible_lang_codes = [lang_code] possible_lang_codes = [lang_code]
@ -625,13 +640,13 @@ def parse_accept_lang_header(lang_string):
functools.lru_cache() to avoid repetitive parsing of common header values. 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 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) return _parse_accept_lang_header(lang_string)
# If there is at least one comma in the value, parse up to the last comma # 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 # before the max length, skipping any truncated parts at the end of the
# header value. # header value.
if (index := lang_string.rfind(",", 0, ACCEPT_LANGUAGE_HEADER_MAX_LENGTH)) > 0: if (index := lang_string.rfind(",", 0, LANGUAGE_CODE_MAX_LENGTH)) > 0:
return _parse_accept_lang_header(lang_string[:index]) return _parse_accept_lang_header(lang_string[:index])
# Don't attempt to parse if there is only one language-range value which is # Don't attempt to parse if there is only one language-range value which is

View File

@ -1155,6 +1155,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 ``lang_code`` is ``'es-ar'`` and ``'es'`` is in :setting:`LANGUAGES` but
``'es-ar'`` isn't. ``'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 If ``strict`` is ``False`` (the default), a country-specific variant may
be returned when neither the language code nor its generic variant is found. 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 For example, if only ``'es-co'`` is in :setting:`LANGUAGES`, that's
@ -1163,6 +1168,11 @@ For a complete discussion on the usage of the following see the
Raises :exc:`LookupError` if nothing is found. 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) .. function:: to_locale(language)
Turns a language name (en-us) into a locale name (en_US). Turns a language name (en-us) into a locale name (en_US).

View File

@ -32,3 +32,18 @@ directory-traversal via certain inputs when calling :meth:`save()
<django.core.files.storage.Storage.save()>`. <django.core.files.storage.Storage.save()>`.
Built-in ``Storage`` sub-classes were not affected by this vulnerability. Built-in ``Storage`` sub-classes were not affected by this vulnerability.
CVE-2024-39614: Potential denial-of-service vulnerability in ``get_supported_language_variant()``
=================================================================================================
:meth:`~django.utils.translation.get_supported_language_variant` was subject to
a potential denial-of-service attack when used with very long strings
containing specific characters.
To mitigate this vulnerability, the language code provided to
:meth:`~django.utils.translation.get_supported_language_variant` is now parsed
up to a maximum length of 500 characters.
When the language code is over 500 characters, a :exc:`ValueError` will now be
raised if ``strict`` is ``True``, or if there is no generic variant and
``strict`` is ``False``.

View File

@ -65,6 +65,7 @@ from django.utils.translation.reloader import (
translation_file_changed, translation_file_changed,
watch_for_translation_changes, watch_for_translation_changes,
) )
from django.utils.translation.trans_real import LANGUAGE_CODE_MAX_LENGTH
from .forms import CompanyForm, I18nForm, SelectDateForm from .forms import CompanyForm, I18nForm, SelectDateForm
from .models import Company, TestModel from .models import Company, TestModel
@ -1888,6 +1889,16 @@ class MiscTests(SimpleTestCase):
g("xyz") g("xyz")
with self.assertRaises(LookupError): with self.assertRaises(LookupError):
g("xy-zz") 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): def test_get_supported_language_variant_null(self):
g = trans_null.get_supported_language_variant g = trans_null.get_supported_language_variant