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 ):
@@ -204,22 +220,8 @@ def test_dangerous_file_names(self):
204220 # a malicious payload with an invalid file name (containing os.sep or
205221 # os.pardir). This similar to what an attacker would need to do when
206222 # trying such an attack.
207- scary_file_names = [
208- "/tmp/hax0rd.txt" , # Absolute path, *nix-style.
209- "C:\\ Windows\\ hax0rd.txt" , # Absolute path, win-style.
210- "C:/Windows/hax0rd.txt" , # Absolute path, broken-style.
211- "\\ tmp\\ hax0rd.txt" , # Absolute path, broken in a different way.
212- "/tmp\\ hax0rd.txt" , # Absolute path, broken by mixing.
213- "subdir/hax0rd.txt" , # Descendant path, *nix-style.
214- "subdir\\ hax0rd.txt" , # Descendant path, win-style.
215- "sub/dir\\ hax0rd.txt" , # Descendant path, mixed.
216- "../../hax0rd.txt" , # Relative path, *nix-style.
217- "..\\ ..\\ hax0rd.txt" , # Relative path, win-style.
218- "../..\\ hax0rd.txt" # Relative path, mixed.
219- ]
220-
221223 payload = client .FakePayload ()
222- for i , name in enumerate (scary_file_names ):
224+ for i , name in enumerate (CANDIDATE_TRAVERSAL_FILE_NAMES ):
223225 payload .write ('\r \n ' .join ([
224226 '--' + client .BOUNDARY ,
225227 'Content-Disposition: form-data; name="file%s"; filename="%s"' % (i , name ),
@@ -239,7 +241,7 @@ def test_dangerous_file_names(self):
239241 response = self .client .request (** r )
240242 # The filenames should have been sanitized by the time it got to the view.
241243 received = response .json ()
242- for i , name in enumerate (scary_file_names ):
244+ for i , name in enumerate (CANDIDATE_TRAVERSAL_FILE_NAMES ):
243245 got = received ["file%s" % i ]
244246 self .assertEqual (got , "hax0rd.txt" )
245247
@@ -517,6 +519,47 @@ def test_filename_case_preservation(self):
517519 # shouldn't differ.
518520 self .assertEqual (os .path .basename (obj .testfile .path ), 'MiXeD_cAsE.txt' )
519521
522+ def test_filename_traversal_upload (self ):
523+ os .makedirs (UPLOAD_TO , exist_ok = True )
524+ self .addCleanup (shutil .rmtree , MEDIA_ROOT )
525+ tests = [
526+ '../test.txt' ,
527+ '../test.txt' ,
528+ ]
529+ for file_name in tests :
530+ with self .subTest (file_name = file_name ):
531+ payload = client .FakePayload ()
532+ payload .write (
533+ '\r \n ' .join ([
534+ '--' + client .BOUNDARY ,
535+ 'Content-Disposition: form-data; name="my_file"; '
536+ 'filename="%s";' % file_name ,
537+ 'Content-Type: text/plain' ,
538+ '' ,
539+ 'file contents.\r \n ' ,
540+ '\r \n --' + client .BOUNDARY + '--\r \n ' ,
541+ ]),
542+ )
543+ r = {
544+ 'CONTENT_LENGTH' : len (payload ),
545+ 'CONTENT_TYPE' : client .MULTIPART_CONTENT ,
546+ 'PATH_INFO' : '/upload_traversal/' ,
547+ 'REQUEST_METHOD' : 'POST' ,
548+ 'wsgi.input' : payload ,
549+ }
550+ response = self .client .request (** r )
551+ result = response .json ()
552+ self .assertEqual (response .status_code , 200 )
553+ self .assertEqual (result ['file_name' ], 'test.txt' )
554+ self .assertIs (
555+ os .path .exists (os .path .join (MEDIA_ROOT , 'test.txt' )),
556+ False ,
557+ )
558+ self .assertIs (
559+ os .path .exists (os .path .join (UPLOAD_TO , 'test.txt' )),
560+ True ,
561+ )
562+
520563
521564@override_settings (MEDIA_ROOT = MEDIA_ROOT )
522565class DirectoryCreationTests (SimpleTestCase ):
@@ -586,6 +629,15 @@ def test_bad_type_content_length(self):
586629 }, StringIO ('x' ), [], 'utf-8' )
587630 self .assertEqual (multipart_parser ._content_length , 0 )
588631
632+ def test_sanitize_file_name (self ):
633+ parser = MultiPartParser ({
634+ 'CONTENT_TYPE' : 'multipart/form-data; boundary=_foo' ,
635+ 'CONTENT_LENGTH' : '1'
636+ }, StringIO ('x' ), [], 'utf-8' )
637+ for file_name in CANDIDATE_TRAVERSAL_FILE_NAMES :
638+ with self .subTest (file_name = file_name ):
639+ self .assertEqual (parser .sanitize_file_name (file_name ), 'hax0rd.txt' )
640+
589641 def test_rfc2231_parsing (self ):
590642 test_data = (
591643 (b"Content-Type: application/x-stuff; title*=us-ascii'en-us'This%20is%20%2A%2A%2Afun%2A%2A%2A" ,
0 commit comments