mirror of
https://github.com/django/django.git
synced 2025-08-04 02:48:35 +00:00
Fixed CVE-2021-28658 -- Fixed potential directory-traversal via uploaded files.
Thanks Claude Paroz for the initial patch. Thanks Dennis Brinkrolf for the report.
This commit is contained in:
parent
78fea27f69
commit
d4d800ca1a
9 changed files with 159 additions and 23 deletions
|
@ -23,6 +23,22 @@ UNICODE_FILENAME = 'test-0123456789_中文_Orléans.jpg'
|
|||
MEDIA_ROOT = sys_tempfile.mkdtemp()
|
||||
UPLOAD_TO = os.path.join(MEDIA_ROOT, 'test_upload')
|
||||
|
||||
CANDIDATE_TRAVERSAL_FILE_NAMES = [
|
||||
'/tmp/hax0rd.txt', # Absolute path, *nix-style.
|
||||
'C:\\Windows\\hax0rd.txt', # Absolute path, win-style.
|
||||
'C:/Windows/hax0rd.txt', # Absolute path, broken-style.
|
||||
'\\tmp\\hax0rd.txt', # Absolute path, broken in a different way.
|
||||
'/tmp\\hax0rd.txt', # Absolute path, broken by mixing.
|
||||
'subdir/hax0rd.txt', # Descendant path, *nix-style.
|
||||
'subdir\\hax0rd.txt', # Descendant path, win-style.
|
||||
'sub/dir\\hax0rd.txt', # Descendant path, mixed.
|
||||
'../../hax0rd.txt', # Relative path, *nix-style.
|
||||
'..\\..\\hax0rd.txt', # Relative path, win-style.
|
||||
'../..\\hax0rd.txt', # Relative path, mixed.
|
||||
'../hax0rd.txt', # HTML entities.
|
||||
'../hax0rd.txt', # HTML entities.
|
||||
]
|
||||
|
||||
|
||||
@override_settings(MEDIA_ROOT=MEDIA_ROOT, ROOT_URLCONF='file_uploads.urls', MIDDLEWARE=[])
|
||||
class FileUploadTests(TestCase):
|
||||
|
@ -251,22 +267,8 @@ class FileUploadTests(TestCase):
|
|||
# a malicious payload with an invalid file name (containing os.sep or
|
||||
# os.pardir). This similar to what an attacker would need to do when
|
||||
# trying such an attack.
|
||||
scary_file_names = [
|
||||
"/tmp/hax0rd.txt", # Absolute path, *nix-style.
|
||||
"C:\\Windows\\hax0rd.txt", # Absolute path, win-style.
|
||||
"C:/Windows/hax0rd.txt", # Absolute path, broken-style.
|
||||
"\\tmp\\hax0rd.txt", # Absolute path, broken in a different way.
|
||||
"/tmp\\hax0rd.txt", # Absolute path, broken by mixing.
|
||||
"subdir/hax0rd.txt", # Descendant path, *nix-style.
|
||||
"subdir\\hax0rd.txt", # Descendant path, win-style.
|
||||
"sub/dir\\hax0rd.txt", # Descendant path, mixed.
|
||||
"../../hax0rd.txt", # Relative path, *nix-style.
|
||||
"..\\..\\hax0rd.txt", # Relative path, win-style.
|
||||
"../..\\hax0rd.txt" # Relative path, mixed.
|
||||
]
|
||||
|
||||
payload = client.FakePayload()
|
||||
for i, name in enumerate(scary_file_names):
|
||||
for i, name in enumerate(CANDIDATE_TRAVERSAL_FILE_NAMES):
|
||||
payload.write('\r\n'.join([
|
||||
'--' + client.BOUNDARY,
|
||||
'Content-Disposition: form-data; name="file%s"; filename="%s"' % (i, name),
|
||||
|
@ -286,7 +288,7 @@ class FileUploadTests(TestCase):
|
|||
response = self.client.request(**r)
|
||||
# The filenames should have been sanitized by the time it got to the view.
|
||||
received = response.json()
|
||||
for i, name in enumerate(scary_file_names):
|
||||
for i, name in enumerate(CANDIDATE_TRAVERSAL_FILE_NAMES):
|
||||
got = received["file%s" % i]
|
||||
self.assertEqual(got, "hax0rd.txt")
|
||||
|
||||
|
@ -597,6 +599,47 @@ class FileUploadTests(TestCase):
|
|||
# shouldn't differ.
|
||||
self.assertEqual(os.path.basename(obj.testfile.path), 'MiXeD_cAsE.txt')
|
||||
|
||||
def test_filename_traversal_upload(self):
|
||||
os.makedirs(UPLOAD_TO, exist_ok=True)
|
||||
self.addCleanup(shutil.rmtree, MEDIA_ROOT)
|
||||
tests = [
|
||||
'../test.txt',
|
||||
'../test.txt',
|
||||
]
|
||||
for file_name in tests:
|
||||
with self.subTest(file_name=file_name):
|
||||
payload = client.FakePayload()
|
||||
payload.write(
|
||||
'\r\n'.join([
|
||||
'--' + client.BOUNDARY,
|
||||
'Content-Disposition: form-data; name="my_file"; '
|
||||
'filename="%s";' % file_name,
|
||||
'Content-Type: text/plain',
|
||||
'',
|
||||
'file contents.\r\n',
|
||||
'\r\n--' + client.BOUNDARY + '--\r\n',
|
||||
]),
|
||||
)
|
||||
r = {
|
||||
'CONTENT_LENGTH': len(payload),
|
||||
'CONTENT_TYPE': client.MULTIPART_CONTENT,
|
||||
'PATH_INFO': '/upload_traversal/',
|
||||
'REQUEST_METHOD': 'POST',
|
||||
'wsgi.input': payload,
|
||||
}
|
||||
response = self.client.request(**r)
|
||||
result = response.json()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(result['file_name'], 'test.txt')
|
||||
self.assertIs(
|
||||
os.path.exists(os.path.join(MEDIA_ROOT, 'test.txt')),
|
||||
False,
|
||||
)
|
||||
self.assertIs(
|
||||
os.path.exists(os.path.join(UPLOAD_TO, 'test.txt')),
|
||||
True,
|
||||
)
|
||||
|
||||
|
||||
@override_settings(MEDIA_ROOT=MEDIA_ROOT)
|
||||
class DirectoryCreationTests(SimpleTestCase):
|
||||
|
@ -666,6 +709,15 @@ class MultiParserTests(SimpleTestCase):
|
|||
}, StringIO('x'), [], 'utf-8')
|
||||
self.assertEqual(multipart_parser._content_length, 0)
|
||||
|
||||
def test_sanitize_file_name(self):
|
||||
parser = MultiPartParser({
|
||||
'CONTENT_TYPE': 'multipart/form-data; boundary=_foo',
|
||||
'CONTENT_LENGTH': '1'
|
||||
}, StringIO('x'), [], 'utf-8')
|
||||
for file_name in CANDIDATE_TRAVERSAL_FILE_NAMES:
|
||||
with self.subTest(file_name=file_name):
|
||||
self.assertEqual(parser.sanitize_file_name(file_name), 'hax0rd.txt')
|
||||
|
||||
def test_rfc2231_parsing(self):
|
||||
test_data = (
|
||||
(b"Content-Type: application/x-stuff; title*=us-ascii'en-us'This%20is%20%2A%2A%2Afun%2A%2A%2A",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue