diff --git a/django/core/management/templates.py b/django/core/management/templates.py index 72db9651b0..e06e6a6307 100644 --- a/django/core/management/templates.py +++ b/django/core/management/templates.py @@ -1,5 +1,4 @@ import argparse -import cgi import mimetypes import os import posixpath @@ -15,6 +14,7 @@ from django.core.management.base import BaseCommand, CommandError from django.core.management.utils import handle_extensions, run_formatters from django.template import Context, Engine from django.utils import archive +from django.utils.http import parse_header_parameters from django.utils.version import get_docs_version @@ -327,7 +327,7 @@ class TemplateCommand(BaseCommand): # Trying to get better name from response headers content_disposition = headers["content-disposition"] if content_disposition: - _, params = cgi.parse_header(content_disposition) + _, params = parse_header_parameters(content_disposition) guessed_filename = params.get("filename") or used_name else: guessed_filename = used_name diff --git a/django/http/multipartparser.py b/django/http/multipartparser.py index f52ff7619e..308fbfa385 100644 --- a/django/http/multipartparser.py +++ b/django/http/multipartparser.py @@ -6,7 +6,6 @@ file upload handlers for processing. """ import base64 import binascii -import cgi import collections import html from urllib.parse import unquote @@ -20,6 +19,7 @@ from django.core.exceptions import ( from django.core.files.uploadhandler import SkipFile, StopFutureHandlers, StopUpload from django.utils.datastructures import MultiValueDict from django.utils.encoding import force_str +from django.utils.regex_helper import _lazy_re_compile __all__ = ("MultiPartParser", "MultiPartParserError", "InputStreamExhausted") @@ -49,6 +49,8 @@ class MultiPartParser: and returns a tuple of ``(MultiValueDict(POST), MultiValueDict(FILES))``. """ + boundary_re = _lazy_re_compile(rb"[ -~]{0,200}[!-~]") + def __init__(self, META, input_data, upload_handlers, encoding=None): """ Initialize the MultiPartParser object. @@ -77,7 +79,7 @@ class MultiPartParser: % force_str(content_type) ) boundary = opts.get("boundary") - if not boundary or not cgi.valid_boundary(boundary): + if not boundary or not self.boundary_re.fullmatch(boundary): raise MultiPartParserError( "Invalid boundary in multipart: %s" % force_str(boundary) ) diff --git a/django/http/request.py b/django/http/request.py index c25fa4379a..4b160bc5f4 100644 --- a/django/http/request.py +++ b/django/http/request.py @@ -1,4 +1,3 @@ -import cgi import codecs import copy from io import BytesIO @@ -22,7 +21,7 @@ from django.utils.datastructures import ( ) from django.utils.encoding import escape_uri_path, iri_to_uri from django.utils.functional import cached_property -from django.utils.http import is_same_domain +from django.utils.http import is_same_domain, parse_header_parameters from django.utils.regex_helper import _lazy_re_compile from .multipartparser import parse_header @@ -97,7 +96,7 @@ class HttpRequest: def _set_content_type_params(self, meta): """Set content_type, content_params, and encoding.""" - self.content_type, self.content_params = cgi.parse_header( + self.content_type, self.content_params = parse_header_parameters( meta.get("CONTENT_TYPE", "") ) if "charset" in self.content_params: diff --git a/django/utils/http.py b/django/utils/http.py index 0292713235..6e2091bf52 100644 --- a/django/utils/http.py +++ b/django/utils/http.py @@ -366,3 +366,36 @@ def escape_leading_slashes(url): if url.startswith("//"): url = "/%2F{}".format(url[2:]) 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): + """ + Parse a Content-type like header. + Return the main content-type and a dictionary of options. + """ + parts = _parseparam(";" + line) + key = parts.__next__() + pdict = {} + for p in parts: + i = p.find("=") + if i >= 0: + name = p[:i].strip().lower() + value = p[i + 1 :].strip() + if len(value) >= 2 and value[0] == value[-1] == '"': + value = value[1:-1] + value = value.replace("\\\\", "\\").replace('\\"', '"') + pdict[name] = value + return key, pdict diff --git a/tests/utils_tests/test_http.py b/tests/utils_tests/test_http.py index 67f3361068..2ba617dfc9 100644 --- a/tests/utils_tests/test_http.py +++ b/tests/utils_tests/test_http.py @@ -12,6 +12,7 @@ from django.utils.http import ( int_to_base36, is_same_domain, parse_etags, + parse_header_parameters, parse_http_date, quote_etag, url_has_allowed_host_and_scheme, @@ -435,3 +436,39 @@ class EscapeLeadingSlashesTests(unittest.TestCase): for url, expected in tests: with self.subTest(url=url): self.assertEqual(escape_leading_slashes(url), expected) + + +class ParseHeaderParameterTests(unittest.TestCase): + def test_basic(self): + tests = [ + ("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"})), + ( + 'text/plain ; charset="us-ascii"', + ("text/plain", {"charset": "us-ascii"}), + ), + ( + 'text/plain ; charset="us-ascii"; another=opt', + ("text/plain", {"charset": "us-ascii", "another": "opt"}), + ), + ( + 'attachment; filename="silly.txt"', + ("attachment", {"filename": "silly.txt"}), + ), + ( + 'attachment; filename="strange;name"', + ("attachment", {"filename": "strange;name"}), + ), + ( + 'attachment; filename="strange;name";size=123;', + ("attachment", {"filename": "strange;name", "size": "123"}), + ), + ( + 'form-data; name="files"; filename="fo\\"o;bar"', + ("form-data", {"name": "files", "filename": 'fo"o;bar'}), + ), + ] + for header, expected in tests: + with self.subTest(header=header): + self.assertEqual(parse_header_parameters(header), expected)