mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +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:
		| @@ -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 MDN’s Accept-Language page. | # attacks. About 10x longer than the longest value shown on MDN’s | ||||||
| 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 | ||||||
|   | |||||||
| @@ -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). | ||||||
|   | |||||||
| @@ -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``. | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user