22
22
MEDIA_ROOT = sys_tempfile .mkdtemp ()
23
23
UPLOAD_TO = os .path .join (MEDIA_ROOT , 'test_upload' )
24
24
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
+
25
41
26
42
@override_settings (MEDIA_ROOT = MEDIA_ROOT , ROOT_URLCONF = 'file_uploads.urls' , MIDDLEWARE = [])
27
43
class FileUploadTests (TestCase ):
@@ -204,22 +220,8 @@ def test_dangerous_file_names(self):
204
220
# a malicious payload with an invalid file name (containing os.sep or
205
221
# os.pardir). This similar to what an attacker would need to do when
206
222
# 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
-
221
223
payload = client .FakePayload ()
222
- for i , name in enumerate (scary_file_names ):
224
+ for i , name in enumerate (CANDIDATE_TRAVERSAL_FILE_NAMES ):
223
225
payload .write ('\r \n ' .join ([
224
226
'--' + client .BOUNDARY ,
225
227
'Content-Disposition: form-data; name="file%s"; filename="%s"' % (i , name ),
@@ -239,7 +241,7 @@ def test_dangerous_file_names(self):
239
241
response = self .client .request (** r )
240
242
# The filenames should have been sanitized by the time it got to the view.
241
243
received = response .json ()
242
- for i , name in enumerate (scary_file_names ):
244
+ for i , name in enumerate (CANDIDATE_TRAVERSAL_FILE_NAMES ):
243
245
got = received ["file%s" % i ]
244
246
self .assertEqual (got , "hax0rd.txt" )
245
247
@@ -517,6 +519,47 @@ def test_filename_case_preservation(self):
517
519
# shouldn't differ.
518
520
self .assertEqual (os .path .basename (obj .testfile .path ), 'MiXeD_cAsE.txt' )
519
521
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
+
520
563
521
564
@override_settings (MEDIA_ROOT = MEDIA_ROOT )
522
565
class DirectoryCreationTests (SimpleTestCase ):
@@ -586,6 +629,15 @@ def test_bad_type_content_length(self):
586
629
}, StringIO ('x' ), [], 'utf-8' )
587
630
self .assertEqual (multipart_parser ._content_length , 0 )
588
631
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
+
589
641
def test_rfc2231_parsing (self ):
590
642
test_data = (
591
643
(b"Content-Type: application/x-stuff; title*=us-ascii'en-us'This%20is%20%2A%2A%2Afun%2A%2A%2A" ,
0 commit comments