Skip to content

Commit

Permalink
[4.2.x] Fixed CVE-2023-23969 -- Prevented DoS with pathological value…
Browse files Browse the repository at this point in the history
…s for Accept-Language.

The parsed values of Accept-Language headers are cached in order to
avoid repetitive parsing. This leads to a potential denial-of-service
vector via excessive memory usage if the raw value of Accept-Language
headers is very large.

Accept-Language headers are now limited to a maximum length in order
to avoid this issue.
  • Loading branch information
ngnpope authored and felixxm committed Feb 1, 2023
1 parent 5e0be08 commit 8a7b22d
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 5 deletions.
31 changes: 30 additions & 1 deletion django/utils/translation/trans_real.py
Expand Up @@ -30,6 +30,11 @@
# magic gettext number to separate context from message
CONTEXT_SEPARATOR = "\x04"

# Maximum number of characters that will be parsed from the Accept-Language
# header to prevent possible denial of service or memory exhaustion attacks.
# About 10x longer than the longest value shown on MDN’s Accept-Language page.
ACCEPT_LANGUAGE_HEADER_MAX_LENGTH = 500

# Format of Accept-Language header values. From RFC 9110 Sections 12.4.2 and
# 12.5.4, and RFC 5646 Section 2.1.
accept_language_re = _lazy_re_compile(
Expand Down Expand Up @@ -582,7 +587,7 @@ def get_language_from_request(request, check_path=False):


@functools.lru_cache(maxsize=1000)
def parse_accept_lang_header(lang_string):
def _parse_accept_lang_header(lang_string):
"""
Parse the lang_string, which is the body of an HTTP Accept-Language
header, and return a tuple of (lang, q-value), ordered by 'q' values.
Expand All @@ -604,3 +609,27 @@ def parse_accept_lang_header(lang_string):
result.append((lang, priority))
result.sort(key=lambda k: k[1], reverse=True)
return tuple(result)


def parse_accept_lang_header(lang_string):
"""
Parse the value of the Accept-Language header up to a maximum length.
The value of the header is truncated to a maximum length to avoid potential
denial of service and memory exhaustion attacks. Excessive memory could be
used if the raw value is very large as it would be cached due to the use of
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 len(lang_string) <= ACCEPT_LANGUAGE_HEADER_MAX_LENGTH:
return _parse_accept_lang_header(lang_string)

# 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
# header value.
if (index := lang_string.rfind(",", 0, ACCEPT_LANGUAGE_HEADER_MAX_LENGTH)) > 0:
return _parse_accept_lang_header(lang_string[:index])

# Don't attempt to parse if there is only one language-range value which is
# longer than the maximum allowed length and so truncated.
return ()
10 changes: 9 additions & 1 deletion docs/releases/3.2.17.txt
Expand Up @@ -6,4 +6,12 @@ Django 3.2.17 release notes

Django 3.2.17 fixes a security issue with severity "moderate" in 3.2.16.

...
CVE-2023-23969: Potential denial-of-service via ``Accept-Language`` headers
===========================================================================

The parsed values of ``Accept-Language`` headers are cached in order to avoid
repetitive parsing. This leads to a potential denial-of-service vector via
excessive memory usage if large header values are sent.

In order to avoid this vulnerability, the ``Accept-Language`` header is now
parsed up to a maximum length.
10 changes: 9 additions & 1 deletion docs/releases/4.0.9.txt
Expand Up @@ -6,4 +6,12 @@ Django 4.0.9 release notes

Django 4.0.9 fixes a security issue with severity "moderate" in 4.0.8.

...
CVE-2023-23969: Potential denial-of-service via ``Accept-Language`` headers
===========================================================================

The parsed values of ``Accept-Language`` headers are cached in order to avoid
repetitive parsing. This leads to a potential denial-of-service vector via
excessive memory usage if large header values are sent.

In order to avoid this vulnerability, the ``Accept-Language`` header is now
parsed up to a maximum length.
14 changes: 12 additions & 2 deletions docs/releases/4.1.6.txt
Expand Up @@ -4,8 +4,18 @@ Django 4.1.6 release notes

*February 1, 2023*

Django 4.1.6 fixes a security issue with severity "moderate" and several bugs
in 4.1.5.
Django 4.1.6 fixes a security issue with severity "moderate" and a bug in
4.1.5.

CVE-2023-23969: Potential denial-of-service via ``Accept-Language`` headers
===========================================================================

The parsed values of ``Accept-Language`` headers are cached in order to avoid
repetitive parsing. This leads to a potential denial-of-service vector via
excessive memory usage if large header values are sent.

In order to avoid this vulnerability, the ``Accept-Language`` header is now
parsed up to a maximum length.

Bugfixes
========
Expand Down
12 changes: 12 additions & 0 deletions tests/i18n/tests.py
Expand Up @@ -1717,6 +1717,14 @@ def test_parse_spec_http_header(self):
("de;q=0.", [("de", 0.0)]),
("en; q=1,", [("en", 1.0)]),
("en; q=1.0, * ; q=0.5", [("en", 1.0), ("*", 0.5)]),
(
"en" + "-x" * 20,
[("en-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x", 1.0)],
),
(
", ".join(["en; q=1.0"] * 20),
[("en", 1.0)] * 20,
),
# Bad headers
("en-gb;q=1.0000", []),
("en;q=0.1234", []),
Expand All @@ -1733,6 +1741,10 @@ def test_parse_spec_http_header(self):
("", []),
("en;q=1e0", []),
("en-au;q=1.0", []),
# Invalid as language-range value too long.
("xxxxxxxx" + "-xxxxxxxx" * 500, []),
# Header value too long, only parse up to limit.
(", ".join(["en; q=1.0"] * 500), [("en", 1.0)] * 45),
]
for value, expected in tests:
with self.subTest(value=value):
Expand Down

0 comments on commit 8a7b22d

Please sign in to comment.