From b3e4494d759202a3b6bf247fd34455bf13be5b80 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Wed, 27 Jul 2022 10:27:42 +0200 Subject: [PATCH] [3.2.x] Fixed CVE-2022-36359 -- Escaped filename in Content-Disposition header. Thanks to Motoyasu Saburi for the report. --- django/http/response.py | 4 +++- docs/releases/3.2.15.txt | 8 ++++++- tests/responses/test_fileresponse.py | 35 ++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/django/http/response.py b/django/http/response.py index 1c22edaff3..73f87d7bda 100644 --- a/django/http/response.py +++ b/django/http/response.py @@ -485,7 +485,9 @@ class FileResponse(StreamingHttpResponse): disposition = 'attachment' if self.as_attachment else 'inline' try: filename.encode('ascii') - file_expr = 'filename="{}"'.format(filename) + file_expr = 'filename="{}"'.format( + filename.replace('\\', '\\\\').replace('"', r'\"') + ) except UnicodeEncodeError: file_expr = "filename*=utf-8''{}".format(quote(filename)) self.headers['Content-Disposition'] = '{}; {}'.format(disposition, file_expr) diff --git a/docs/releases/3.2.15.txt b/docs/releases/3.2.15.txt index a7a56ae965..281444ecf2 100644 --- a/docs/releases/3.2.15.txt +++ b/docs/releases/3.2.15.txt @@ -6,4 +6,10 @@ Django 3.2.15 release notes Django 3.2.15 fixes a security issue with severity "high" in 3.2.14. -... +CVE-2022-36359: Potential reflected file download vulnerability in ``FileResponse`` +=================================================================================== + +An application may have been vulnerable to a reflected file download (RFD) +attack that sets the Content-Disposition header of a +:class:`~django.http.FileResponse` when the ``filename`` was derived from +user-supplied input. The ``filename`` is now escaped to avoid this possibility. diff --git a/tests/responses/test_fileresponse.py b/tests/responses/test_fileresponse.py index 46d407bdf5..b4ef82ef3e 100644 --- a/tests/responses/test_fileresponse.py +++ b/tests/responses/test_fileresponse.py @@ -89,3 +89,38 @@ class FileResponseTests(SimpleTestCase): response.headers['Content-Disposition'], "attachment; filename*=utf-8''%E7%A5%9D%E6%82%A8%E5%B9%B3%E5%AE%89.odt" ) + + def test_content_disposition_escaping(self): + # fmt: off + tests = [ + ( + 'multi-part-one";\" dummy".txt', + r"multi-part-one\";\" dummy\".txt" + ), + ] + # fmt: on + # Non-escape sequence backslashes are path segments on Windows, and are + # eliminated by an os.path.basename() check in FileResponse. + if sys.platform != "win32": + # fmt: off + tests += [ + ( + 'multi-part-one\\";\" dummy".txt', + r"multi-part-one\\\";\" dummy\".txt" + ), + ( + 'multi-part-one\\";\\\" dummy".txt', + r"multi-part-one\\\";\\\" dummy\".txt" + ) + ] + # fmt: on + for filename, escaped in tests: + with self.subTest(filename=filename, escaped=escaped): + response = FileResponse( + io.BytesIO(b"binary content"), filename=filename, as_attachment=True + ) + response.close() + self.assertEqual( + response.headers["Content-Disposition"], + f'attachment; filename="{escaped}"', + )