1
0
mirror of https://github.com/django/django.git synced 2025-07-04 09:49:12 +00:00

[3.2.x] Fixed CVE-2022-36359 -- Escaped filename in Content-Disposition header.

Thanks to Motoyasu Saburi for the report.
This commit is contained in:
Carlton Gibson 2022-07-27 10:27:42 +02:00
parent cb7fbac9f8
commit b3e4494d75
3 changed files with 45 additions and 2 deletions

View File

@ -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)

View File

@ -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.

View File

@ -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}"',
)