diff --git a/django/utils/http.py b/django/utils/http.py index bf783562dd..2098cbc36d 100644 --- a/django/utils/http.py +++ b/django/utils/http.py @@ -362,10 +362,19 @@ def content_disposition_header(as_attachment, filename): disposition = "attachment" if as_attachment else "inline" try: filename.encode("ascii") + is_ascii = True + except UnicodeEncodeError: + is_ascii = False + # Quoted strings can contain horizontal tabs, space characters, and + # characters from 0x21 to 0x7e, except 0x22 (`"`) and 0x5C (`\`) which + # can still be expressed but must be escaped with their own `\`. + # https://datatracker.ietf.org/doc/html/rfc9110#name-quoted-strings + quotable_characters = r"^[\t \x21-\x7e]*$" + if is_ascii and re.match(quotable_characters, filename): file_expr = 'filename="{}"'.format( filename.replace("\\", "\\\\").replace('"', r"\"") ) - except UnicodeEncodeError: + else: file_expr = "filename*=utf-8''{}".format(quote(filename)) return f"{disposition}; {file_expr}" elif as_attachment: diff --git a/tests/utils_tests/test_http.py b/tests/utils_tests/test_http.py index 68df04696a..c04f0a03d7 100644 --- a/tests/utils_tests/test_http.py +++ b/tests/utils_tests/test_http.py @@ -511,6 +511,7 @@ class ContentDispositionHeaderTests(unittest.TestCase): (True, '"espécimen" filename'), "attachment; filename*=utf-8''%22esp%C3%A9cimen%22%20filename", ), + ((True, "some\nfile"), "attachment; filename*=utf-8''some%0Afile"), ) for (is_attachment, filename), expected in tests: