2323MEDIA_ROOT = sys_tempfile .mkdtemp ()
2424UPLOAD_TO = os .path .join (MEDIA_ROOT , 'test_upload' )
2525
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+
2642
2743@override_settings (MEDIA_ROOT = MEDIA_ROOT , ROOT_URLCONF = 'file_uploads.urls' , MIDDLEWARE = [])
2844class FileUploadTests (TestCase ):
@@ -251,22 +267,8 @@ def test_dangerous_file_names(self):
251267 # a malicious payload with an invalid file name (containing os.sep or
252268 # os.pardir). This similar to what an attacker would need to do when
253269 # 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-
268270 payload = client .FakePayload ()
269- for i , name in enumerate (scary_file_names ):
271+ for i , name in enumerate (CANDIDATE_TRAVERSAL_FILE_NAMES ):
270272 payload .write ('\r \n ' .join ([
271273 '--' + client .BOUNDARY ,
272274 'Content-Disposition: form-data; name="file%s"; filename="%s"' % (i , name ),
@@ -286,7 +288,7 @@ def test_dangerous_file_names(self):
286288 response = self .client .request (** r )
287289 # The filenames should have been sanitized by the time it got to the view.
288290 received = response .json ()
289- for i , name in enumerate (scary_file_names ):
291+ for i , name in enumerate (CANDIDATE_TRAVERSAL_FILE_NAMES ):
290292 got = received ["file%s" % i ]
291293 self .assertEqual (got , "hax0rd.txt" )
292294
@@ -597,6 +599,47 @@ def test_filename_case_preservation(self):
597599 # shouldn't differ.
598600 self .assertEqual (os .path .basename (obj .testfile .path ), 'MiXeD_cAsE.txt' )
599601
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+
600643
601644@override_settings (MEDIA_ROOT = MEDIA_ROOT )
602645class DirectoryCreationTests (SimpleTestCase ):
@@ -666,6 +709,15 @@ def test_bad_type_content_length(self):
666709 }, StringIO ('x' ), [], 'utf-8' )
667710 self .assertEqual (multipart_parser ._content_length , 0 )
668711
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+
669721 def test_rfc2231_parsing (self ):
670722 test_data = (
671723 (b"Content-Type: application/x-stuff; title*=us-ascii'en-us'This%20is%20%2A%2A%2Afun%2A%2A%2A" ,
0 commit comments