From 424e0d86973d88b402b55f20884938715aad740b Mon Sep 17 00:00:00 2001 From: Natalia <124304+nessita@users.noreply.github.com> Date: Mon, 15 Sep 2025 11:45:20 -0300 Subject: [PATCH] Fixed #36520 -- Reverted "Fixed #35440 -- Simplified parse_header_parameters by leveraging stdlid's Message." This partially reverts commit 9aabe7eae3eeb3e64c5a0f3687118cd806158550. The simplification of parse_header_parameters using stdlib's Message is reverted due to a performance regression. The check for the header maximum length remains in place, per Security Team guidance. Thanks to David Smith for reporting the regression, and Jacob Walls for the review. --- django/utils/http.py | 55 ++++++++++++++++++++++++---------- tests/utils_tests/test_http.py | 5 ++-- 2 files changed, 42 insertions(+), 18 deletions(-) diff --git a/django/utils/http.py b/django/utils/http.py index 504f28c678..fe0b21f150 100644 --- a/django/utils/http.py +++ b/django/utils/http.py @@ -3,9 +3,8 @@ import re import unicodedata from binascii import Error as BinasciiError from datetime import UTC, datetime -from email.message import Message -from email.utils import collapse_rfc2231_value, formatdate -from urllib.parse import quote +from email.utils import formatdate +from urllib.parse import quote, unquote from urllib.parse import urlencode as original_urlencode from urllib.parse import urlsplit @@ -316,6 +315,19 @@ def escape_leading_slashes(url): return url +def _parseparam(s): + while s[:1] == ";": + s = s[1:] + end = s.find(";") + while end > 0 and (s.count('"', 0, end) - s.count('\\"', 0, end)) % 2: + end = s.find(";", end + 1) + if end < 0: + end = len(s) + f = s[:end] + yield f.strip() + s = s[end:] + + def parse_header_parameters(line, max_length=MAX_HEADER_LENGTH): """ Parse a Content-type like header. @@ -323,21 +335,34 @@ def parse_header_parameters(line, max_length=MAX_HEADER_LENGTH): If `line` is longer than `max_length`, `ValueError` is raised. """ - if max_length is not None and line and len(line) > max_length: + if not line: + return "", {} + + if max_length is not None and len(line) > max_length: raise ValueError("Unable to parse header parameters (value too long).") - m = Message() - m["content-type"] = line - params = m.get_params() - + parts = _parseparam(";" + line) + key = parts.__next__().lower() pdict = {} - key = params.pop(0)[0].lower() - for name, value in params: - if not name: - continue - if isinstance(value, tuple): - value = collapse_rfc2231_value(value) - pdict[name] = value + for p in parts: + i = p.find("=") + if i >= 0: + has_encoding = False + name = p[:i].strip().lower() + if name.endswith("*"): + # Embedded lang/encoding, like "filename*=UTF-8''file.ext". + # https://tools.ietf.org/html/rfc2231#section-4 + name = name[:-1] + if p.count("'") == 2: + has_encoding = True + value = p[i + 1 :].strip() + if len(value) >= 2 and value[0] == value[-1] == '"': + value = value[1:-1] + value = value.replace("\\\\", "\\").replace('\\"', '"') + if has_encoding: + encoding, lang, value = value.split("'") + value = unquote(value, encoding=encoding) + pdict[name] = value return key, pdict diff --git a/tests/utils_tests/test_http.py b/tests/utils_tests/test_http.py index 95ec2fc516..58a4b40f3e 100644 --- a/tests/utils_tests/test_http.py +++ b/tests/utils_tests/test_http.py @@ -442,7 +442,7 @@ class ParseHeaderParameterTests(unittest.TestCase): def test_basic(self): tests = [ ("", ("", {})), - (None, ("none", {})), + (None, ("", {})), ("text/plain", ("text/plain", {})), ("text/vnd.just.made.this.up ; ", ("text/vnd.just.made.this.up", {})), ("text/plain;charset=us-ascii", ("text/plain", {"charset": "us-ascii"})), @@ -507,13 +507,12 @@ class ParseHeaderParameterTests(unittest.TestCase): """ Test wrongly formatted RFC 2231 headers (missing double single quotes). Parsing should not crash (#24209). - But stdlib email still decodes (#35440). """ test_data = ( ( "Content-Type: application/x-stuff; " "title*='This%20is%20%2A%2A%2Afun%2A%2A%2A", - "'This is ***fun***", + "'This%20is%20%2A%2A%2Afun%2A%2A%2A", ), ("Content-Type: application/x-stuff; title*='foo.html", "'foo.html"), ("Content-Type: application/x-stuff; title*=bar.html", "bar.html"),