22
22
MEDIA_ROOT = sys_tempfile.mkdtemp()
23
23
UPLOAD_TO = os.path.join(MEDIA_ROOT, 'test_upload')
24
24
25
+
CANDIDATE_TRAVERSAL_FILE_NAMES = [
26
+
'/tmp/hax0rd.txt', # Absolute path, *nix-style.
27
+
'C:\\Windows\\hax0rd.txt', # Absolute path, win-style.
28
+
'C:/Windows/hax0rd.txt', # Absolute path, broken-style.
29
+
'\\tmp\\hax0rd.txt', # Absolute path, broken in a different way.
30
+
'/tmp\\hax0rd.txt', # Absolute path, broken by mixing.
31
+
'subdir/hax0rd.txt', # Descendant path, *nix-style.
32
+
'subdir\\hax0rd.txt', # Descendant path, win-style.
33
+
'sub/dir\\hax0rd.txt', # Descendant path, mixed.
34
+
'../../hax0rd.txt', # Relative path, *nix-style.
35
+
'..\\..\\hax0rd.txt', # Relative path, win-style.
36
+
'../..\\hax0rd.txt', # Relative path, mixed.
37
+
'../hax0rd.txt', # HTML entities.
38
+
'../hax0rd.txt', # HTML entities.
39
+
]
40
+
25
41
26
42
@override_settings(MEDIA_ROOT=MEDIA_ROOT, ROOT_URLCONF='file_uploads.urls', MIDDLEWARE=[])
27
43
class FileUploadTests(TestCase):
@@ -204,22 +220,8 @@ def test_dangerous_file_names(self):
204
220
# a malicious payload with an invalid file name (containing os.sep or
205
221
# os.pardir). This similar to what an attacker would need to do when
206
222
# trying such an attack.
207
-
scary_file_names = [
208
-
"/tmp/hax0rd.txt", # Absolute path, *nix-style.
209
-
"C:\\Windows\\hax0rd.txt", # Absolute path, win-style.
210
-
"C:/Windows/hax0rd.txt", # Absolute path, broken-style.
211
-
"\\tmp\\hax0rd.txt", # Absolute path, broken in a different way.
212
-
"/tmp\\hax0rd.txt", # Absolute path, broken by mixing.
213
-
"subdir/hax0rd.txt", # Descendant path, *nix-style.
214
-
"subdir\\hax0rd.txt", # Descendant path, win-style.
215
-
"sub/dir\\hax0rd.txt", # Descendant path, mixed.
216
-
"../../hax0rd.txt", # Relative path, *nix-style.
217
-
"..\\..\\hax0rd.txt", # Relative path, win-style.
218
-
"../..\\hax0rd.txt" # Relative path, mixed.
219
-
]
220
-
221
223
payload = client.FakePayload()
222
-
for i, name in enumerate(scary_file_names):
224
+
for i, name in enumerate(CANDIDATE_TRAVERSAL_FILE_NAMES):
223
225
payload.write('\r\n'.join([
224
226
'--' + client.BOUNDARY,
225
227
'Content-Disposition: form-data; name="file%s"; filename="%s"' % (i, name),
@@ -239,7 +241,7 @@ def test_dangerous_file_names(self):
239
241
response = self.client.request(**r)
240
242
# The filenames should have been sanitized by the time it got to the view.
241
243
received = response.json()
242
-
for i, name in enumerate(scary_file_names):
244
+
for i, name in enumerate(CANDIDATE_TRAVERSAL_FILE_NAMES):
243
245
got = received["file%s" % i]
244
246
self.assertEqual(got, "hax0rd.txt")
245
247
@@ -517,6 +519,47 @@ def test_filename_case_preservation(self):
517
519
# shouldn't differ.
518
520
self.assertEqual(os.path.basename(obj.testfile.path), 'MiXeD_cAsE.txt')
519
521
522
+
def test_filename_traversal_upload(self):
523
+
os.makedirs(UPLOAD_TO, exist_ok=True)
524
+
self.addCleanup(shutil.rmtree, MEDIA_ROOT)
525
+
tests = [
526
+
'../test.txt',
527
+
'../test.txt',
528
+
]
529
+
for file_name in tests:
530
+
with self.subTest(file_name=file_name):
531
+
payload = client.FakePayload()
532
+
payload.write(
533
+
'\r\n'.join([
534
+
'--' + client.BOUNDARY,
535
+
'Content-Disposition: form-data; name="my_file"; '
536
+
'filename="%s";' % file_name,
537
+
'Content-Type: text/plain',
538
+
'',
539
+
'file contents.\r\n',
540
+
'\r\n--' + client.BOUNDARY + '--\r\n',
541
+
]),
542
+
)
543
+
r = {
544
+
'CONTENT_LENGTH': len(payload),
545
+
'CONTENT_TYPE': client.MULTIPART_CONTENT,
546
+
'PATH_INFO': '/upload_traversal/',
547
+
'REQUEST_METHOD': 'POST',
548
+
'wsgi.input': payload,
549
+
}
550
+
response = self.client.request(**r)
551
+
result = response.json()
552
+
self.assertEqual(response.status_code, 200)
553
+
self.assertEqual(result['file_name'], 'test.txt')
554
+
self.assertIs(
555
+
os.path.exists(os.path.join(MEDIA_ROOT, 'test.txt')),
556
+
False,
557
+
)
558
+
self.assertIs(
559
+
os.path.exists(os.path.join(UPLOAD_TO, 'test.txt')),
560
+
True,
561
+
)
562
+
520
563
521
564
@override_settings(MEDIA_ROOT=MEDIA_ROOT)
522
565
class DirectoryCreationTests(SimpleTestCase):
@@ -586,6 +629,15 @@ def test_bad_type_content_length(self):
586
629
}, StringIO('x'), [], 'utf-8')
587
630
self.assertEqual(multipart_parser._content_length, 0)
588
631
632
+
def test_sanitize_file_name(self):
633
+
parser = MultiPartParser({
634
+
'CONTENT_TYPE': 'multipart/form-data; boundary=_foo',
635
+
'CONTENT_LENGTH': '1'
636
+
}, StringIO('x'), [], 'utf-8')
637
+
for file_name in CANDIDATE_TRAVERSAL_FILE_NAMES:
638
+
with self.subTest(file_name=file_name):
639
+
self.assertEqual(parser.sanitize_file_name(file_name), 'hax0rd.txt')
640
+
589
641
def test_rfc2231_parsing(self):
590
642
test_data = (
591
643
(b"Content-Type: application/x-stuff; title*=us-ascii'en-us'This%20is%20%2A%2A%2Afun%2A%2A%2A",
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4