123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249 |
- import os
- from django.core.exceptions import SuspiciousFileOperation
- from django.core.files.base import ContentFile
- from django.core.files.storage import FileSystemStorage, Storage
- from django.db.models import FileField
- from django.test import SimpleTestCase
- class AWSS3Storage(Storage):
- """
- Simulate an AWS S3 storage which uses Unix-like paths and allows any
- characters in file names but where there aren't actual folders but just
- keys.
- """
- prefix = "mys3folder/"
- def _save(self, name, content):
- """
- This method is important to test that Storage.save() doesn't replace
- '\' with '/' (rather FileSystemStorage.save() does).
- """
- return name
- def get_valid_name(self, name):
- return name
- def get_available_name(self, name, max_length=None):
- return name
- def generate_filename(self, filename):
- """
- This is the method that's important to override when using S3 so that
- os.path() isn't called, which would break S3 keys.
- """
- return self.prefix + self.get_valid_name(filename)
- class StorageGenerateFilenameTests(SimpleTestCase):
- """Tests for base Storage's generate_filename method."""
- storage_class = Storage
- def test_valid_names(self):
- storage = self.storage_class()
- name = "UnTRIVíAL @fil$ena#me!"
- valid_name = storage.get_valid_name(name)
- candidates = [
- (name, valid_name),
- (f"././././././{name}", valid_name),
- (f"some/path/{name}", f"some/path/{valid_name}"),
- (f"some/./path/./{name}", f"some/path/{valid_name}"),
- (f"././some/././path/./{name}", f"some/path/{valid_name}"),
- (f".\\.\\.\\.\\.\\.\\{name}", valid_name),
- (f"some\\path\\{name}", f"some/path/{valid_name}"),
- (f"some\\.\\path\\.\\{name}", f"some/path/{valid_name}"),
- (f".\\.\\some\\.\\.\\path\\.\\{name}", f"some/path/{valid_name}"),
- ]
- for name, expected in candidates:
- with self.subTest(name=name):
- result = storage.generate_filename(name)
- self.assertEqual(result, os.path.normpath(expected))
- class FileSystemStorageGenerateFilenameTests(StorageGenerateFilenameTests):
- storage_class = FileSystemStorage
- class GenerateFilenameStorageTests(SimpleTestCase):
- def test_storage_dangerous_paths(self):
- candidates = [
- ("/tmp/..", ".."),
- ("\\tmp\\..", ".."),
- ("/tmp/.", "."),
- ("\\tmp\\.", "."),
- ("..", ".."),
- (".", "."),
- ("", ""),
- ]
- s = FileSystemStorage()
- s_overwrite = FileSystemStorage(allow_overwrite=True)
- msg = "Could not derive file name from '%s'"
- for file_name, base_name in candidates:
- with self.subTest(file_name=file_name):
- with self.assertRaisesMessage(SuspiciousFileOperation, msg % base_name):
- s.get_available_name(file_name)
- with self.assertRaisesMessage(SuspiciousFileOperation, msg % base_name):
- s_overwrite.get_available_name(file_name)
- with self.assertRaisesMessage(SuspiciousFileOperation, msg % base_name):
- s.generate_filename(file_name)
- def test_storage_dangerous_paths_dir_name(self):
- candidates = [
- ("../path", ".."),
- ("..\\path", ".."),
- ("tmp/../path", "tmp/.."),
- ("tmp\\..\\path", "tmp/.."),
- ("/tmp/../path", "/tmp/.."),
- ("\\tmp\\..\\path", "/tmp/.."),
- ]
- s = FileSystemStorage()
- s_overwrite = FileSystemStorage(allow_overwrite=True)
- for file_name, path in candidates:
- msg = "Detected path traversal attempt in '%s'" % path
- with self.subTest(file_name=file_name):
- with self.assertRaisesMessage(SuspiciousFileOperation, msg):
- s.get_available_name(file_name)
- with self.assertRaisesMessage(SuspiciousFileOperation, msg):
- s_overwrite.get_available_name(file_name)
- with self.assertRaisesMessage(SuspiciousFileOperation, msg):
- s.generate_filename(file_name)
- def test_filefield_dangerous_filename(self):
- candidates = [
- ("..", "some/folder/.."),
- (".", "some/folder/."),
- ("", "some/folder/"),
- ("???", "???"),
- ("$.$.$", "$.$.$"),
- ]
- f = FileField(upload_to="some/folder/")
- for file_name, msg_file_name in candidates:
- msg = f"Could not derive file name from '{msg_file_name}'"
- with self.subTest(file_name=file_name):
- with self.assertRaisesMessage(SuspiciousFileOperation, msg):
- f.generate_filename(None, file_name)
- def test_filefield_dangerous_filename_dot_segments(self):
- f = FileField(upload_to="some/folder/")
- msg = "Detected path traversal attempt in 'some/folder/../path'"
- with self.assertRaisesMessage(SuspiciousFileOperation, msg):
- f.generate_filename(None, "../path")
- def test_filefield_generate_filename_absolute_path(self):
- f = FileField(upload_to="some/folder/")
- candidates = [
- "/tmp/path",
- "/tmp/../path",
- ]
- for file_name in candidates:
- msg = f"Detected path traversal attempt in '{file_name}'"
- with self.subTest(file_name=file_name):
- with self.assertRaisesMessage(SuspiciousFileOperation, msg):
- f.generate_filename(None, file_name)
- def test_filefield_generate_filename(self):
- f = FileField(upload_to="some/folder/")
- self.assertEqual(
- f.generate_filename(None, "test with space.txt"),
- os.path.normpath("some/folder/test_with_space.txt"),
- )
- def test_filefield_generate_filename_with_upload_to(self):
- def upload_to(instance, filename):
- return "some/folder/" + filename
- f = FileField(upload_to=upload_to)
- self.assertEqual(
- f.generate_filename(None, "test with space.txt"),
- os.path.normpath("some/folder/test_with_space.txt"),
- )
- def test_filefield_generate_filename_upload_to_overrides_dangerous_filename(self):
- def upload_to(instance, filename):
- return "test.txt"
- f = FileField(upload_to=upload_to)
- candidates = [
- "/tmp/.",
- "/tmp/..",
- "/tmp/../path",
- "/tmp/path",
- "some/folder/",
- "some/folder/.",
- "some/folder/..",
- "some/folder/???",
- "some/folder/$.$.$",
- "some/../test.txt",
- "",
- ]
- for file_name in candidates:
- with self.subTest(file_name=file_name):
- self.assertEqual(f.generate_filename(None, file_name), "test.txt")
- def test_filefield_generate_filename_upload_to_absolute_path(self):
- def upload_to(instance, filename):
- return "/tmp/" + filename
- f = FileField(upload_to=upload_to)
- candidates = [
- "path",
- "../path",
- "???",
- "$.$.$",
- ]
- for file_name in candidates:
- msg = f"Detected path traversal attempt in '/tmp/{file_name}'"
- with self.subTest(file_name=file_name):
- with self.assertRaisesMessage(SuspiciousFileOperation, msg):
- f.generate_filename(None, file_name)
- def test_filefield_generate_filename_upload_to_dangerous_filename(self):
- def upload_to(instance, filename):
- return "/tmp/" + filename
- f = FileField(upload_to=upload_to)
- candidates = ["..", ".", ""]
- for file_name in candidates:
- msg = f"Could not derive file name from '/tmp/{file_name}'"
- with self.subTest(file_name=file_name):
- with self.assertRaisesMessage(SuspiciousFileOperation, msg):
- f.generate_filename(None, file_name)
- def test_filefield_awss3_storage(self):
- """
- Simulate a FileField with an S3 storage which uses keys rather than
- folders and names. FileField and Storage shouldn't have any os.path()
- calls that break the key.
- """
- storage = AWSS3Storage()
- folder = "not/a/folder/"
- f = FileField(upload_to=folder, storage=storage)
- key = "my-file-key\\with odd characters"
- data = ContentFile("test")
- expected_key = AWSS3Storage.prefix + folder + key
- # Simulate call to f.save()
- result_key = f.generate_filename(None, key)
- self.assertEqual(result_key, expected_key)
- result_key = storage.save(result_key, data)
- self.assertEqual(result_key, expected_key)
- # Repeat test with a callable.
- def upload_to(instance, filename):
- # Return a non-normalized path on purpose.
- return folder + filename
- f = FileField(upload_to=upload_to, storage=storage)
- # Simulate call to f.save()
- result_key = f.generate_filename(None, key)
- self.assertEqual(result_key, expected_key)
- result_key = storage.save(result_key, data)
- self.assertEqual(result_key, expected_key)
|