88from io import BytesIO , StringIO
99from urllib .parse import quote
1010
11+ from django .core .exceptions import SuspiciousFileOperation
1112from django .core .files import temp as tempfile
12- from django .core .files .uploadedfile import SimpleUploadedFile
13+ from django .core .files .uploadedfile import SimpleUploadedFile , UploadedFile
1314from django .http .multipartparser import (
1415 MultiPartParser , MultiPartParserError , parse_header ,
1516)
3738 '../hax0rd.txt' , # HTML entities.
3839]
3940
41+ CANDIDATE_INVALID_FILE_NAMES = [
42+ '/tmp/' , # Directory, *nix-style.
43+ 'c:\\ tmp\\ ' , # Directory, win-style.
44+ '/tmp/.' , # Directory dot, *nix-style.
45+ 'c:\\ tmp\\ .' , # Directory dot, *nix-style.
46+ '/tmp/..' , # Parent directory, *nix-style.
47+ 'c:\\ tmp\\ ..' , # Parent directory, win-style.
48+ '' , # Empty filename.
49+ ]
50+
4051
4152@override_settings (MEDIA_ROOT = MEDIA_ROOT , ROOT_URLCONF = 'file_uploads.urls' , MIDDLEWARE = [])
4253class FileUploadTests (TestCase ):
@@ -52,6 +63,22 @@ def tearDownClass(cls):
5263 shutil .rmtree (MEDIA_ROOT )
5364 super ().tearDownClass ()
5465
66+ def test_upload_name_is_validated (self ):
67+ candidates = [
68+ '/tmp/' ,
69+ '/tmp/..' ,
70+ '/tmp/.' ,
71+ ]
72+ if sys .platform == 'win32' :
73+ candidates .extend ([
74+ 'c:\\ tmp\\ ' ,
75+ 'c:\\ tmp\\ ..' ,
76+ 'c:\\ tmp\\ .' ,
77+ ])
78+ for file_name in candidates :
79+ with self .subTest (file_name = file_name ):
80+ self .assertRaises (SuspiciousFileOperation , UploadedFile , name = file_name )
81+
5582 def test_simple_upload (self ):
5683 with open (__file__ , 'rb' ) as fp :
5784 post_data = {
@@ -631,6 +658,15 @@ def test_sanitize_file_name(self):
631658 with self .subTest (file_name = file_name ):
632659 self .assertEqual (parser .sanitize_file_name (file_name ), 'hax0rd.txt' )
633660
661+ def test_sanitize_invalid_file_name (self ):
662+ parser = MultiPartParser ({
663+ 'CONTENT_TYPE' : 'multipart/form-data; boundary=_foo' ,
664+ 'CONTENT_LENGTH' : '1' ,
665+ }, StringIO ('x' ), [], 'utf-8' )
666+ for file_name in CANDIDATE_INVALID_FILE_NAMES :
667+ with self .subTest (file_name = file_name ):
668+ self .assertIsNone (parser .sanitize_file_name (file_name ))
669+
634670 def test_rfc2231_parsing (self ):
635671 test_data = (
636672 (b"Content-Type: application/x-stuff; title*=us-ascii'en-us'This%20is%20%2A%2A%2Afun%2A%2A%2A" ,
0 commit comments