2222MEDIA_ROOT = sys_tempfile .mkdtemp ()
2323UPLOAD_TO = os .path .join (MEDIA_ROOT , 'test_upload' )
2424
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+
2541
2642@override_settings (MEDIA_ROOT = MEDIA_ROOT , ROOT_URLCONF = 'file_uploads.urls' , MIDDLEWARE = [])
2743class FileUploadTests (TestCase ):
@@ -250,22 +266,8 @@ def test_dangerous_file_names(self):
250266 # a malicious payload with an invalid file name (containing os.sep or
251267 # os.pardir). This similar to what an attacker would need to do when
252268 # 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-
267269 payload = client .FakePayload ()
268- for i , name in enumerate (scary_file_names ):
270+ for i , name in enumerate (CANDIDATE_TRAVERSAL_FILE_NAMES ):
269271 payload .write ('\r \n ' .join ([
270272 '--' + client .BOUNDARY ,
271273 'Content-Disposition: form-data; name="file%s"; filename="%s"' % (i , name ),
@@ -285,7 +287,7 @@ def test_dangerous_file_names(self):
285287 response = self .client .request (** r )
286288 # The filenames should have been sanitized by the time it got to the view.
287289 received = response .json ()
288- for i , name in enumerate (scary_file_names ):
290+ for i , name in enumerate (CANDIDATE_TRAVERSAL_FILE_NAMES ):
289291 got = received ["file%s" % i ]
290292 self .assertEqual (got , "hax0rd.txt" )
291293
@@ -564,6 +566,47 @@ def test_filename_case_preservation(self):
564566 # shouldn't differ.
565567 self .assertEqual (os .path .basename (obj .testfile .path ), 'MiXeD_cAsE.txt' )
566568
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+
567610
568611@override_settings (MEDIA_ROOT = MEDIA_ROOT )
569612class DirectoryCreationTests (SimpleTestCase ):
@@ -633,6 +676,15 @@ def test_bad_type_content_length(self):
633676 }, StringIO ('x' ), [], 'utf-8' )
634677 self .assertEqual (multipart_parser ._content_length , 0 )
635678
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+
636688 def test_rfc2231_parsing (self ):
637689 test_data = (
638690 (b"Content-Type: application/x-stuff; title*=us-ascii'en-us'This%20is%20%2A%2A%2Afun%2A%2A%2A" ,
0 commit comments