Skip to content

Commit 18cb41f

Browse files
authored
Merge pull request #211 from tari/196-rfd
Guard against reflected file download
2 parents 1b294f0 + e7e25e6 commit 18cb41f

4 files changed

Lines changed: 26 additions & 4 deletions

File tree

django_downloadview/response.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,16 @@ def content_disposition(filename):
7272
"""
7373
if not filename:
7474
return "attachment"
75-
ascii_filename = encode_basename_ascii(filename)
75+
# ASCII filenames are quoted and must ensure escape sequences
76+
# in the filename won't break out of the quoted header value
77+
# which can permit a reflected file download attack. The UTF-8
78+
# version is immune because it's not quoted.
79+
ascii_filename = (
80+
encode_basename_ascii(filename).replace("\\", "\\\\").replace('"', r'\"')
81+
)
7682
utf8_filename = encode_basename_utf8(filename)
7783
if ascii_filename == utf8_filename: # ASCII only.
84+
7885
return f'attachment; filename="{ascii_filename}"'
7986
else:
8087
return (

tests/packaging.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Tests around project's distribution and packaging."""
2+
import importlib.metadata
23
import os
34
import unittest
45

tests/response.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,16 @@ def test_content_disposition_encoding(self):
1919
self.assertIn(
2020
"filename*=UTF-8''espac%C3%A9%20.txt", headers["Content-Disposition"]
2121
)
22+
23+
def test_content_disposition_escaping(self):
24+
"""Content-Disposition headers escape special characters."""
25+
response = DownloadResponse(
26+
"fake file",
27+
attachment=True,
28+
basename=r'"malicious\file.exe'
29+
)
30+
headers = response.default_headers
31+
self.assertIn(
32+
r'filename="\"malicious\\file.exe"',
33+
headers["Content-Disposition"]
34+
)

tox.ini

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ deps =
3131
commands =
3232
pip install -e .
3333
pip install -e demo
34-
# doctests
34+
# doctests and unit tests
3535
pytest --cov=django_downloadview --cov=demoproject {posargs}
36-
# all other test cases
37-
coverage run --append {envbindir}/demo test {posargs: tests demoproject}
36+
# demo project integration tests
37+
coverage run --append {envbindir}/demo test {posargs: demoproject}
3838
coverage xml
3939
pip freeze
4040
ignore_outcome =
@@ -76,3 +76,4 @@ source = django_downloadview,demo
7676
[pytest]
7777
DJANGO_SETTINGS_MODULE = demoproject.settings
7878
addopts = --doctest-modules --ignore=docs/
79+
python_files = tests/*.py

0 commit comments

Comments
 (0)