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+ ]
39+
2540
2641@override_settings (MEDIA_ROOT = MEDIA_ROOT , ROOT_URLCONF = 'file_uploads.urls' , MIDDLEWARE = [])
2742class FileUploadTests (TestCase ):
@@ -205,22 +220,8 @@ def test_dangerous_file_names(self):
205220 # a malicious payload with an invalid file name (containing os.sep or
206221 # os.pardir). This similar to what an attacker would need to do when
207222 # trying such an attack.
208- scary_file_names = [
209- "/tmp/hax0rd.txt" , # Absolute path, *nix-style.
210- "C:\\ Windows\\ hax0rd.txt" , # Absolute path, win-style.
211- "C:/Windows/hax0rd.txt" , # Absolute path, broken-style.
212- "\\ tmp\\ hax0rd.txt" , # Absolute path, broken in a different way.
213- "/tmp\\ hax0rd.txt" , # Absolute path, broken by mixing.
214- "subdir/hax0rd.txt" , # Descendant path, *nix-style.
215- "subdir\\ hax0rd.txt" , # Descendant path, win-style.
216- "sub/dir\\ hax0rd.txt" , # Descendant path, mixed.
217- "../../hax0rd.txt" , # Relative path, *nix-style.
218- "..\\ ..\\ hax0rd.txt" , # Relative path, win-style.
219- "../..\\ hax0rd.txt" # Relative path, mixed.
220- ]
221-
222223 payload = client .FakePayload ()
223- for i , name in enumerate (scary_file_names ):
224+ for i , name in enumerate (CANDIDATE_TRAVERSAL_FILE_NAMES ):
224225 payload .write ('\r \n ' .join ([
225226 '--' + client .BOUNDARY ,
226227 'Content-Disposition: form-data; name="file%s"; filename="%s"' % (i , name ),
@@ -240,7 +241,7 @@ def test_dangerous_file_names(self):
240241 response = self .client .request (** r )
241242 # The filenames should have been sanitized by the time it got to the view.
242243 received = response .json ()
243- for i , name in enumerate (scary_file_names ):
244+ for i , name in enumerate (CANDIDATE_TRAVERSAL_FILE_NAMES ):
244245 got = received ["file%s" % i ]
245246 self .assertEqual (got , "hax0rd.txt" )
246247
@@ -518,6 +519,36 @@ def test_filename_case_preservation(self):
518519 # shouldn't differ.
519520 self .assertEqual (os .path .basename (obj .testfile .path ), 'MiXeD_cAsE.txt' )
520521
522+ def test_filename_traversal_upload (self ):
523+ os .makedirs (UPLOAD_TO , exist_ok = True )
524+ self .addCleanup (shutil .rmtree , MEDIA_ROOT )
525+ file_name = '../test.txt' ,
526+ payload = client .FakePayload ()
527+ payload .write (
528+ '\r \n ' .join ([
529+ '--' + client .BOUNDARY ,
530+ 'Content-Disposition: form-data; name="my_file"; '
531+ 'filename="%s";' % file_name ,
532+ 'Content-Type: text/plain' ,
533+ '' ,
534+ 'file contents.\r \n ' ,
535+ '\r \n --' + client .BOUNDARY + '--\r \n ' ,
536+ ]),
537+ )
538+ r = {
539+ 'CONTENT_LENGTH' : len (payload ),
540+ 'CONTENT_TYPE' : client .MULTIPART_CONTENT ,
541+ 'PATH_INFO' : '/upload_traversal/' ,
542+ 'REQUEST_METHOD' : 'POST' ,
543+ 'wsgi.input' : payload ,
544+ }
545+ response = self .client .request (** r )
546+ result = response .json ()
547+ self .assertEqual (response .status_code , 200 )
548+ self .assertEqual (result ['file_name' ], 'test.txt' )
549+ self .assertIs (os .path .exists (os .path .join (MEDIA_ROOT , 'test.txt' )), False )
550+ self .assertIs (os .path .exists (os .path .join (UPLOAD_TO , 'test.txt' )), True )
551+
521552
522553@override_settings (MEDIA_ROOT = MEDIA_ROOT )
523554class DirectoryCreationTests (SimpleTestCase ):
@@ -591,6 +622,15 @@ def test_bad_type_content_length(self):
591622 }, StringIO ('x' ), [], 'utf-8' )
592623 self .assertEqual (multipart_parser ._content_length , 0 )
593624
625+ def test_sanitize_file_name (self ):
626+ parser = MultiPartParser ({
627+ 'CONTENT_TYPE' : 'multipart/form-data; boundary=_foo' ,
628+ 'CONTENT_LENGTH' : '1'
629+ }, StringIO ('x' ), [], 'utf-8' )
630+ for file_name in CANDIDATE_TRAVERSAL_FILE_NAMES :
631+ with self .subTest (file_name = file_name ):
632+ self .assertEqual (parser .sanitize_file_name (file_name ), 'hax0rd.txt' )
633+
594634 def test_rfc2231_parsing (self ):
595635 test_data = (
596636 (b"Content-Type: application/x-stuff; title*=us-ascii'en-us'This%20is%20%2A%2A%2Afun%2A%2A%2A" ,
0 commit comments