23
23
MEDIA_ROOT = sys_tempfile .mkdtemp ()
24
24
UPLOAD_TO = os .path .join (MEDIA_ROOT , 'test_upload' )
25
25
26
+ CANDIDATE_TRAVERSAL_FILE_NAMES = [
27
+ '/tmp/hax0rd.txt' , # Absolute path, *nix-style.
28
+ 'C:\\ Windows\\ hax0rd.txt' , # Absolute path, win-style.
29
+ 'C:/Windows/hax0rd.txt' , # Absolute path, broken-style.
30
+ '\\ tmp\\ hax0rd.txt' , # Absolute path, broken in a different way.
31
+ '/tmp\\ hax0rd.txt' , # Absolute path, broken by mixing.
32
+ 'subdir/hax0rd.txt' , # Descendant path, *nix-style.
33
+ 'subdir\\ hax0rd.txt' , # Descendant path, win-style.
34
+ 'sub/dir\\ hax0rd.txt' , # Descendant path, mixed.
35
+ '../../hax0rd.txt' , # Relative path, *nix-style.
36
+ '..\\ ..\\ hax0rd.txt' , # Relative path, win-style.
37
+ '../..\\ hax0rd.txt' , # Relative path, mixed.
38
+ '../hax0rd.txt' , # HTML entities.
39
+ '../hax0rd.txt' , # HTML entities.
40
+ ]
41
+
26
42
27
43
@override_settings (MEDIA_ROOT = MEDIA_ROOT , ROOT_URLCONF = 'file_uploads.urls' , MIDDLEWARE = [])
28
44
class FileUploadTests (TestCase ):
@@ -251,22 +267,8 @@ def test_dangerous_file_names(self):
251
267
# a malicious payload with an invalid file name (containing os.sep or
252
268
# os.pardir). This similar to what an attacker would need to do when
253
269
# trying such an attack.
254
- scary_file_names = [
255
- "/tmp/hax0rd.txt" , # Absolute path, *nix-style.
256
- "C:\\ Windows\\ hax0rd.txt" , # Absolute path, win-style.
257
- "C:/Windows/hax0rd.txt" , # Absolute path, broken-style.
258
- "\\ tmp\\ hax0rd.txt" , # Absolute path, broken in a different way.
259
- "/tmp\\ hax0rd.txt" , # Absolute path, broken by mixing.
260
- "subdir/hax0rd.txt" , # Descendant path, *nix-style.
261
- "subdir\\ hax0rd.txt" , # Descendant path, win-style.
262
- "sub/dir\\ hax0rd.txt" , # Descendant path, mixed.
263
- "../../hax0rd.txt" , # Relative path, *nix-style.
264
- "..\\ ..\\ hax0rd.txt" , # Relative path, win-style.
265
- "../..\\ hax0rd.txt" # Relative path, mixed.
266
- ]
267
-
268
270
payload = client .FakePayload ()
269
- for i , name in enumerate (scary_file_names ):
271
+ for i , name in enumerate (CANDIDATE_TRAVERSAL_FILE_NAMES ):
270
272
payload .write ('\r \n ' .join ([
271
273
'--' + client .BOUNDARY ,
272
274
'Content-Disposition: form-data; name="file%s"; filename="%s"' % (i , name ),
@@ -286,7 +288,7 @@ def test_dangerous_file_names(self):
286
288
response = self .client .request (** r )
287
289
# The filenames should have been sanitized by the time it got to the view.
288
290
received = response .json ()
289
- for i , name in enumerate (scary_file_names ):
291
+ for i , name in enumerate (CANDIDATE_TRAVERSAL_FILE_NAMES ):
290
292
got = received ["file%s" % i ]
291
293
self .assertEqual (got , "hax0rd.txt" )
292
294
@@ -597,6 +599,47 @@ def test_filename_case_preservation(self):
597
599
# shouldn't differ.
598
600
self .assertEqual (os .path .basename (obj .testfile .path ), 'MiXeD_cAsE.txt' )
599
601
602
+ def test_filename_traversal_upload (self ):
603
+ os .makedirs (UPLOAD_TO , exist_ok = True )
604
+ self .addCleanup (shutil .rmtree , MEDIA_ROOT )
605
+ tests = [
606
+ '../test.txt' ,
607
+ '../test.txt' ,
608
+ ]
609
+ for file_name in tests :
610
+ with self .subTest (file_name = file_name ):
611
+ payload = client .FakePayload ()
612
+ payload .write (
613
+ '\r \n ' .join ([
614
+ '--' + client .BOUNDARY ,
615
+ 'Content-Disposition: form-data; name="my_file"; '
616
+ 'filename="%s";' % file_name ,
617
+ 'Content-Type: text/plain' ,
618
+ '' ,
619
+ 'file contents.\r \n ' ,
620
+ '\r \n --' + client .BOUNDARY + '--\r \n ' ,
621
+ ]),
622
+ )
623
+ r = {
624
+ 'CONTENT_LENGTH' : len (payload ),
625
+ 'CONTENT_TYPE' : client .MULTIPART_CONTENT ,
626
+ 'PATH_INFO' : '/upload_traversal/' ,
627
+ 'REQUEST_METHOD' : 'POST' ,
628
+ 'wsgi.input' : payload ,
629
+ }
630
+ response = self .client .request (** r )
631
+ result = response .json ()
632
+ self .assertEqual (response .status_code , 200 )
633
+ self .assertEqual (result ['file_name' ], 'test.txt' )
634
+ self .assertIs (
635
+ os .path .exists (os .path .join (MEDIA_ROOT , 'test.txt' )),
636
+ False ,
637
+ )
638
+ self .assertIs (
639
+ os .path .exists (os .path .join (UPLOAD_TO , 'test.txt' )),
640
+ True ,
641
+ )
642
+
600
643
601
644
@override_settings (MEDIA_ROOT = MEDIA_ROOT )
602
645
class DirectoryCreationTests (SimpleTestCase ):
@@ -666,6 +709,15 @@ def test_bad_type_content_length(self):
666
709
}, StringIO ('x' ), [], 'utf-8' )
667
710
self .assertEqual (multipart_parser ._content_length , 0 )
668
711
712
+ def test_sanitize_file_name (self ):
713
+ parser = MultiPartParser ({
714
+ 'CONTENT_TYPE' : 'multipart/form-data; boundary=_foo' ,
715
+ 'CONTENT_LENGTH' : '1'
716
+ }, StringIO ('x' ), [], 'utf-8' )
717
+ for file_name in CANDIDATE_TRAVERSAL_FILE_NAMES :
718
+ with self .subTest (file_name = file_name ):
719
+ self .assertEqual (parser .sanitize_file_name (file_name ), 'hax0rd.txt' )
720
+
669
721
def test_rfc2231_parsing (self ):
670
722
test_data = (
671
723
(b"Content-Type: application/x-stuff; title*=us-ascii'en-us'This%20is%20%2A%2A%2Afun%2A%2A%2A" ,
0 commit comments