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 ):
@@ -250,22 +266,8 @@ def test_dangerous_file_names(self):
250
266
# a malicious payload with an invalid file name (containing os.sep or
251
267
# os.pardir). This similar to what an attacker would need to do when
252
268
# trying such an attack.
253
- scary_file_names = [
254
- "/tmp/hax0rd.txt" , # Absolute path, *nix-style.
255
- "C:\\ Windows\\ hax0rd.txt" , # Absolute path, win-style.
256
- "C:/Windows/hax0rd.txt" , # Absolute path, broken-style.
257
- "\\ tmp\\ hax0rd.txt" , # Absolute path, broken in a different way.
258
- "/tmp\\ hax0rd.txt" , # Absolute path, broken by mixing.
259
- "subdir/hax0rd.txt" , # Descendant path, *nix-style.
260
- "subdir\\ hax0rd.txt" , # Descendant path, win-style.
261
- "sub/dir\\ hax0rd.txt" , # Descendant path, mixed.
262
- "../../hax0rd.txt" , # Relative path, *nix-style.
263
- "..\\ ..\\ hax0rd.txt" , # Relative path, win-style.
264
- "../..\\ hax0rd.txt" # Relative path, mixed.
265
- ]
266
-
267
269
payload = client .FakePayload ()
268
- for i , name in enumerate (scary_file_names ):
270
+ for i , name in enumerate (CANDIDATE_TRAVERSAL_FILE_NAMES ):
269
271
payload .write ('\r \n ' .join ([
270
272
'--' + client .BOUNDARY ,
271
273
'Content-Disposition: form-data; name="file%s"; filename="%s"' % (i , name ),
@@ -285,7 +287,7 @@ def test_dangerous_file_names(self):
285
287
response = self .client .request (** r )
286
288
# The filenames should have been sanitized by the time it got to the view.
287
289
received = response .json ()
288
- for i , name in enumerate (scary_file_names ):
290
+ for i , name in enumerate (CANDIDATE_TRAVERSAL_FILE_NAMES ):
289
291
got = received ["file%s" % i ]
290
292
self .assertEqual (got , "hax0rd.txt" )
291
293
@@ -564,6 +566,47 @@ def test_filename_case_preservation(self):
564
566
# shouldn't differ.
565
567
self .assertEqual (os .path .basename (obj .testfile .path ), 'MiXeD_cAsE.txt' )
566
568
569
+ def test_filename_traversal_upload (self ):
570
+ os .makedirs (UPLOAD_TO , exist_ok = True )
571
+ self .addCleanup (shutil .rmtree , MEDIA_ROOT )
572
+ tests = [
573
+ '../test.txt' ,
574
+ '../test.txt' ,
575
+ ]
576
+ for file_name in tests :
577
+ with self .subTest (file_name = file_name ):
578
+ payload = client .FakePayload ()
579
+ payload .write (
580
+ '\r \n ' .join ([
581
+ '--' + client .BOUNDARY ,
582
+ 'Content-Disposition: form-data; name="my_file"; '
583
+ 'filename="%s";' % file_name ,
584
+ 'Content-Type: text/plain' ,
585
+ '' ,
586
+ 'file contents.\r \n ' ,
587
+ '\r \n --' + client .BOUNDARY + '--\r \n ' ,
588
+ ]),
589
+ )
590
+ r = {
591
+ 'CONTENT_LENGTH' : len (payload ),
592
+ 'CONTENT_TYPE' : client .MULTIPART_CONTENT ,
593
+ 'PATH_INFO' : '/upload_traversal/' ,
594
+ 'REQUEST_METHOD' : 'POST' ,
595
+ 'wsgi.input' : payload ,
596
+ }
597
+ response = self .client .request (** r )
598
+ result = response .json ()
599
+ self .assertEqual (response .status_code , 200 )
600
+ self .assertEqual (result ['file_name' ], 'test.txt' )
601
+ self .assertIs (
602
+ os .path .exists (os .path .join (MEDIA_ROOT , 'test.txt' )),
603
+ False ,
604
+ )
605
+ self .assertIs (
606
+ os .path .exists (os .path .join (UPLOAD_TO , 'test.txt' )),
607
+ True ,
608
+ )
609
+
567
610
568
611
@override_settings (MEDIA_ROOT = MEDIA_ROOT )
569
612
class DirectoryCreationTests (SimpleTestCase ):
@@ -633,6 +676,15 @@ def test_bad_type_content_length(self):
633
676
}, StringIO ('x' ), [], 'utf-8' )
634
677
self .assertEqual (multipart_parser ._content_length , 0 )
635
678
679
+ def test_sanitize_file_name (self ):
680
+ parser = MultiPartParser ({
681
+ 'CONTENT_TYPE' : 'multipart/form-data; boundary=_foo' ,
682
+ 'CONTENT_LENGTH' : '1'
683
+ }, StringIO ('x' ), [], 'utf-8' )
684
+ for file_name in CANDIDATE_TRAVERSAL_FILE_NAMES :
685
+ with self .subTest (file_name = file_name ):
686
+ self .assertEqual (parser .sanitize_file_name (file_name ), 'hax0rd.txt' )
687
+
636
688
def test_rfc2231_parsing (self ):
637
689
test_data = (
638
690
(b"Content-Type: application/x-stuff; title*=us-ascii'en-us'This%20is%20%2A%2A%2Afun%2A%2A%2A" ,
0 commit comments