Fixed CVE-2021-31542 -- Tightened path & file name sanitation in file uploads.
This commit is contained in:
parent
8de4ca74ba
commit
0b79eb3691
|
@ -1,4 +1,5 @@
|
||||||
import os
|
import os
|
||||||
|
import pathlib
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
|
@ -6,6 +7,7 @@ from django.conf import settings
|
||||||
from django.core.exceptions import SuspiciousFileOperation
|
from django.core.exceptions import SuspiciousFileOperation
|
||||||
from django.core.files import File, locks
|
from django.core.files import File, locks
|
||||||
from django.core.files.move import file_move_safe
|
from django.core.files.move import file_move_safe
|
||||||
|
from django.core.files.utils import validate_file_name
|
||||||
from django.core.signals import setting_changed
|
from django.core.signals import setting_changed
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils._os import safe_join
|
from django.utils._os import safe_join
|
||||||
|
@ -74,6 +76,9 @@ class Storage:
|
||||||
available for new content to be written to.
|
available for new content to be written to.
|
||||||
"""
|
"""
|
||||||
dir_name, file_name = os.path.split(name)
|
dir_name, file_name = os.path.split(name)
|
||||||
|
if '..' in pathlib.PurePath(dir_name).parts:
|
||||||
|
raise SuspiciousFileOperation("Detected path traversal attempt in '%s'" % dir_name)
|
||||||
|
validate_file_name(file_name)
|
||||||
file_root, file_ext = os.path.splitext(file_name)
|
file_root, file_ext = os.path.splitext(file_name)
|
||||||
# If the filename already exists, generate an alternative filename
|
# If the filename already exists, generate an alternative filename
|
||||||
# until it doesn't exist.
|
# until it doesn't exist.
|
||||||
|
@ -105,6 +110,8 @@ class Storage:
|
||||||
"""
|
"""
|
||||||
# `filename` may include a path as returned by FileField.upload_to.
|
# `filename` may include a path as returned by FileField.upload_to.
|
||||||
dirname, filename = os.path.split(filename)
|
dirname, filename = os.path.split(filename)
|
||||||
|
if '..' in pathlib.PurePath(dirname).parts:
|
||||||
|
raise SuspiciousFileOperation("Detected path traversal attempt in '%s'" % dirname)
|
||||||
return os.path.normpath(os.path.join(dirname, self.get_valid_name(filename)))
|
return os.path.normpath(os.path.join(dirname, self.get_valid_name(filename)))
|
||||||
|
|
||||||
def path(self, name):
|
def path(self, name):
|
||||||
|
|
|
@ -8,6 +8,7 @@ from io import BytesIO
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.files import temp as tempfile
|
from django.core.files import temp as tempfile
|
||||||
from django.core.files.base import File
|
from django.core.files.base import File
|
||||||
|
from django.core.files.utils import validate_file_name
|
||||||
|
|
||||||
__all__ = ('UploadedFile', 'TemporaryUploadedFile', 'InMemoryUploadedFile',
|
__all__ = ('UploadedFile', 'TemporaryUploadedFile', 'InMemoryUploadedFile',
|
||||||
'SimpleUploadedFile')
|
'SimpleUploadedFile')
|
||||||
|
@ -47,6 +48,8 @@ class UploadedFile(File):
|
||||||
ext = ext[:255]
|
ext = ext[:255]
|
||||||
name = name[:255 - len(ext)] + ext
|
name = name[:255 - len(ext)] + ext
|
||||||
|
|
||||||
|
name = validate_file_name(name)
|
||||||
|
|
||||||
self._name = name
|
self._name = name
|
||||||
|
|
||||||
name = property(_get_name, _set_name)
|
name = property(_get_name, _set_name)
|
||||||
|
|
|
@ -1,3 +1,19 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.exceptions import SuspiciousFileOperation
|
||||||
|
|
||||||
|
|
||||||
|
def validate_file_name(name):
|
||||||
|
if name != os.path.basename(name):
|
||||||
|
raise SuspiciousFileOperation("File name '%s' includes path elements" % name)
|
||||||
|
|
||||||
|
# Remove potentially dangerous names
|
||||||
|
if name in {'', '.', '..'}:
|
||||||
|
raise SuspiciousFileOperation("Could not derive file name from '%s'" % name)
|
||||||
|
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
class FileProxyMixin:
|
class FileProxyMixin:
|
||||||
"""
|
"""
|
||||||
A mixin class used to forward file methods to an underlaying file
|
A mixin class used to forward file methods to an underlaying file
|
||||||
|
|
|
@ -6,6 +6,7 @@ from django.core import checks
|
||||||
from django.core.files.base import File
|
from django.core.files.base import File
|
||||||
from django.core.files.images import ImageFile
|
from django.core.files.images import ImageFile
|
||||||
from django.core.files.storage import Storage, default_storage
|
from django.core.files.storage import Storage, default_storage
|
||||||
|
from django.core.files.utils import validate_file_name
|
||||||
from django.db.models import signals
|
from django.db.models import signals
|
||||||
from django.db.models.fields import Field
|
from django.db.models.fields import Field
|
||||||
from django.db.models.query_utils import DeferredAttribute
|
from django.db.models.query_utils import DeferredAttribute
|
||||||
|
@ -312,6 +313,7 @@ class FileField(Field):
|
||||||
Until the storage layer, all file paths are expected to be Unix style
|
Until the storage layer, all file paths are expected to be Unix style
|
||||||
(with forward slashes).
|
(with forward slashes).
|
||||||
"""
|
"""
|
||||||
|
filename = validate_file_name(filename)
|
||||||
if callable(self.upload_to):
|
if callable(self.upload_to):
|
||||||
filename = self.upload_to(instance, filename)
|
filename = self.upload_to(instance, filename)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -9,7 +9,6 @@ import binascii
|
||||||
import cgi
|
import cgi
|
||||||
import collections
|
import collections
|
||||||
import html
|
import html
|
||||||
import os
|
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -306,10 +305,25 @@ class MultiPartParser:
|
||||||
break
|
break
|
||||||
|
|
||||||
def sanitize_file_name(self, file_name):
|
def sanitize_file_name(self, file_name):
|
||||||
|
"""
|
||||||
|
Sanitize the filename of an upload.
|
||||||
|
|
||||||
|
Remove all possible path separators, even though that might remove more
|
||||||
|
than actually required by the target system. Filenames that could
|
||||||
|
potentially cause problems (current/parent dir) are also discarded.
|
||||||
|
|
||||||
|
It should be noted that this function could still return a "filepath"
|
||||||
|
like "C:some_file.txt" which is handled later on by the storage layer.
|
||||||
|
So while this function does sanitize filenames to some extent, the
|
||||||
|
resulting filename should still be considered as untrusted user input.
|
||||||
|
"""
|
||||||
file_name = html.unescape(file_name)
|
file_name = html.unescape(file_name)
|
||||||
# Cleanup Windows-style path separators.
|
file_name = file_name.rsplit('/')[-1]
|
||||||
file_name = file_name[file_name.rfind('\\') + 1:].strip()
|
file_name = file_name.rsplit('\\')[-1]
|
||||||
return os.path.basename(file_name)
|
|
||||||
|
if file_name in {'', '.', '..'}:
|
||||||
|
return None
|
||||||
|
return file_name
|
||||||
|
|
||||||
IE_sanitize = sanitize_file_name
|
IE_sanitize = sanitize_file_name
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import unicodedata
|
||||||
from gzip import GzipFile
|
from gzip import GzipFile
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
|
from django.core.exceptions import SuspiciousFileOperation
|
||||||
from django.utils.functional import SimpleLazyObject, keep_lazy_text, lazy
|
from django.utils.functional import SimpleLazyObject, keep_lazy_text, lazy
|
||||||
from django.utils.regex_helper import _lazy_re_compile
|
from django.utils.regex_helper import _lazy_re_compile
|
||||||
from django.utils.translation import gettext as _, gettext_lazy, pgettext
|
from django.utils.translation import gettext as _, gettext_lazy, pgettext
|
||||||
|
@ -221,7 +222,7 @@ class Truncator(SimpleLazyObject):
|
||||||
|
|
||||||
|
|
||||||
@keep_lazy_text
|
@keep_lazy_text
|
||||||
def get_valid_filename(s):
|
def get_valid_filename(name):
|
||||||
"""
|
"""
|
||||||
Return the given string converted to a string that can be used for a clean
|
Return the given string converted to a string that can be used for a clean
|
||||||
filename. Remove leading and trailing spaces; convert other spaces to
|
filename. Remove leading and trailing spaces; convert other spaces to
|
||||||
|
@ -230,8 +231,11 @@ def get_valid_filename(s):
|
||||||
>>> get_valid_filename("john's portrait in 2004.jpg")
|
>>> get_valid_filename("john's portrait in 2004.jpg")
|
||||||
'johns_portrait_in_2004.jpg'
|
'johns_portrait_in_2004.jpg'
|
||||||
"""
|
"""
|
||||||
s = str(s).strip().replace(' ', '_')
|
s = str(name).strip().replace(' ', '_')
|
||||||
return re.sub(r'(?u)[^-\w.]', '', s)
|
s = re.sub(r'(?u)[^-\w.]', '', s)
|
||||||
|
if s in {'', '.', '..'}:
|
||||||
|
raise SuspiciousFileOperation("Could not derive file name from '%s'" % name)
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
@keep_lazy_text
|
@keep_lazy_text
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
===========================
|
||||||
|
Django 2.2.21 release notes
|
||||||
|
===========================
|
||||||
|
|
||||||
|
*May 4, 2021*
|
||||||
|
|
||||||
|
Django 2.2.21 fixes a security issue in 2.2.20.
|
||||||
|
|
||||||
|
CVE-2021-31542: Potential directory-traversal via uploaded files
|
||||||
|
================================================================
|
||||||
|
|
||||||
|
``MultiPartParser``, ``UploadedFile``, and ``FieldFile`` allowed
|
||||||
|
directory-traversal via uploaded files with suitably crafted file names.
|
||||||
|
|
||||||
|
In order to mitigate this risk, stricter basename and path sanitation is now
|
||||||
|
applied. Specifically, empty file names and paths with dot segments will be
|
||||||
|
rejected.
|
|
@ -0,0 +1,17 @@
|
||||||
|
==========================
|
||||||
|
Django 3.1.9 release notes
|
||||||
|
==========================
|
||||||
|
|
||||||
|
*May 4, 2021*
|
||||||
|
|
||||||
|
Django 3.1.9 fixes a security issue in 3.1.8.
|
||||||
|
|
||||||
|
CVE-2021-31542: Potential directory-traversal via uploaded files
|
||||||
|
================================================================
|
||||||
|
|
||||||
|
``MultiPartParser``, ``UploadedFile``, and ``FieldFile`` allowed
|
||||||
|
directory-traversal via uploaded files with suitably crafted file names.
|
||||||
|
|
||||||
|
In order to mitigate this risk, stricter basename and path sanitation is now
|
||||||
|
applied. Specifically, empty file names and paths with dot segments will be
|
||||||
|
rejected.
|
|
@ -2,9 +2,19 @@
|
||||||
Django 3.2.1 release notes
|
Django 3.2.1 release notes
|
||||||
==========================
|
==========================
|
||||||
|
|
||||||
*Expected May 4, 2021*
|
*May 4, 2021*
|
||||||
|
|
||||||
Django 3.2.1 fixes several bugs in 3.2.
|
Django 3.2.1 fixes a security issue and several bugs in 3.2.
|
||||||
|
|
||||||
|
CVE-2021-31542: Potential directory-traversal via uploaded files
|
||||||
|
================================================================
|
||||||
|
|
||||||
|
``MultiPartParser``, ``UploadedFile``, and ``FieldFile`` allowed
|
||||||
|
directory-traversal via uploaded files with suitably crafted file names.
|
||||||
|
|
||||||
|
In order to mitigate this risk, stricter basename and path sanitation is now
|
||||||
|
applied. Specifically, empty file names and paths with dot segments will be
|
||||||
|
rejected.
|
||||||
|
|
||||||
Bugfixes
|
Bugfixes
|
||||||
========
|
========
|
||||||
|
|
|
@ -40,6 +40,7 @@ versions of the documentation contain the release notes for any later releases.
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 1
|
:maxdepth: 1
|
||||||
|
|
||||||
|
3.1.9
|
||||||
3.1.8
|
3.1.8
|
||||||
3.1.7
|
3.1.7
|
||||||
3.1.6
|
3.1.6
|
||||||
|
@ -76,6 +77,7 @@ versions of the documentation contain the release notes for any later releases.
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 1
|
:maxdepth: 1
|
||||||
|
|
||||||
|
2.2.21
|
||||||
2.2.20
|
2.2.20
|
||||||
2.2.19
|
2.2.19
|
||||||
2.2.18
|
2.2.18
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from django.core.exceptions import SuspiciousFileOperation
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.core.files.storage import Storage
|
from django.core.files.storage import FileSystemStorage, Storage
|
||||||
from django.db.models import FileField
|
from django.db.models import FileField
|
||||||
from django.test import SimpleTestCase
|
from django.test import SimpleTestCase
|
||||||
|
|
||||||
|
@ -36,6 +37,44 @@ class AWSS3Storage(Storage):
|
||||||
|
|
||||||
|
|
||||||
class GenerateFilenameStorageTests(SimpleTestCase):
|
class GenerateFilenameStorageTests(SimpleTestCase):
|
||||||
|
def test_storage_dangerous_paths(self):
|
||||||
|
candidates = [
|
||||||
|
('/tmp/..', '..'),
|
||||||
|
('/tmp/.', '.'),
|
||||||
|
('', ''),
|
||||||
|
]
|
||||||
|
s = FileSystemStorage()
|
||||||
|
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.generate_filename(file_name)
|
||||||
|
|
||||||
|
def test_storage_dangerous_paths_dir_name(self):
|
||||||
|
file_name = '/tmp/../path'
|
||||||
|
s = FileSystemStorage()
|
||||||
|
msg = "Detected path traversal attempt in '/tmp/..'"
|
||||||
|
with self.assertRaisesMessage(SuspiciousFileOperation, msg):
|
||||||
|
s.get_available_name(file_name)
|
||||||
|
with self.assertRaisesMessage(SuspiciousFileOperation, msg):
|
||||||
|
s.generate_filename(file_name)
|
||||||
|
|
||||||
|
def test_filefield_dangerous_filename(self):
|
||||||
|
candidates = ['..', '.', '', '???', '$.$.$']
|
||||||
|
f = FileField(upload_to='some/folder/')
|
||||||
|
msg = "Could not derive file name from '%s'"
|
||||||
|
for file_name in candidates:
|
||||||
|
with self.subTest(file_name=file_name):
|
||||||
|
with self.assertRaisesMessage(SuspiciousFileOperation, msg % file_name):
|
||||||
|
f.generate_filename(None, file_name)
|
||||||
|
|
||||||
|
def test_filefield_dangerous_filename_dir(self):
|
||||||
|
f = FileField(upload_to='some/folder/')
|
||||||
|
msg = "File name '/tmp/path' includes path elements"
|
||||||
|
with self.assertRaisesMessage(SuspiciousFileOperation, msg):
|
||||||
|
f.generate_filename(None, '/tmp/path')
|
||||||
|
|
||||||
def test_filefield_generate_filename(self):
|
def test_filefield_generate_filename(self):
|
||||||
f = FileField(upload_to='some/folder/')
|
f = FileField(upload_to='some/folder/')
|
||||||
|
|
|
@ -9,8 +9,9 @@ from io import BytesIO, StringIO
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
from django.core.exceptions import SuspiciousFileOperation
|
||||||
from django.core.files import temp as tempfile
|
from django.core.files import temp as tempfile
|
||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
from django.core.files.uploadedfile import SimpleUploadedFile, UploadedFile
|
||||||
from django.http.multipartparser import (
|
from django.http.multipartparser import (
|
||||||
FILE, MultiPartParser, MultiPartParserError, Parser, parse_header,
|
FILE, MultiPartParser, MultiPartParserError, Parser, parse_header,
|
||||||
)
|
)
|
||||||
|
@ -39,6 +40,16 @@ CANDIDATE_TRAVERSAL_FILE_NAMES = [
|
||||||
'../hax0rd.txt', # HTML entities.
|
'../hax0rd.txt', # HTML entities.
|
||||||
]
|
]
|
||||||
|
|
||||||
|
CANDIDATE_INVALID_FILE_NAMES = [
|
||||||
|
'/tmp/', # Directory, *nix-style.
|
||||||
|
'c:\\tmp\\', # Directory, win-style.
|
||||||
|
'/tmp/.', # Directory dot, *nix-style.
|
||||||
|
'c:\\tmp\\.', # Directory dot, *nix-style.
|
||||||
|
'/tmp/..', # Parent directory, *nix-style.
|
||||||
|
'c:\\tmp\\..', # Parent directory, win-style.
|
||||||
|
'', # Empty filename.
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@override_settings(MEDIA_ROOT=MEDIA_ROOT, ROOT_URLCONF='file_uploads.urls', MIDDLEWARE=[])
|
@override_settings(MEDIA_ROOT=MEDIA_ROOT, ROOT_URLCONF='file_uploads.urls', MIDDLEWARE=[])
|
||||||
class FileUploadTests(TestCase):
|
class FileUploadTests(TestCase):
|
||||||
|
@ -53,6 +64,22 @@ class FileUploadTests(TestCase):
|
||||||
shutil.rmtree(MEDIA_ROOT)
|
shutil.rmtree(MEDIA_ROOT)
|
||||||
super().tearDownClass()
|
super().tearDownClass()
|
||||||
|
|
||||||
|
def test_upload_name_is_validated(self):
|
||||||
|
candidates = [
|
||||||
|
'/tmp/',
|
||||||
|
'/tmp/..',
|
||||||
|
'/tmp/.',
|
||||||
|
]
|
||||||
|
if sys.platform == 'win32':
|
||||||
|
candidates.extend([
|
||||||
|
'c:\\tmp\\',
|
||||||
|
'c:\\tmp\\..',
|
||||||
|
'c:\\tmp\\.',
|
||||||
|
])
|
||||||
|
for file_name in candidates:
|
||||||
|
with self.subTest(file_name=file_name):
|
||||||
|
self.assertRaises(SuspiciousFileOperation, UploadedFile, name=file_name)
|
||||||
|
|
||||||
def test_simple_upload(self):
|
def test_simple_upload(self):
|
||||||
with open(__file__, 'rb') as fp:
|
with open(__file__, 'rb') as fp:
|
||||||
post_data = {
|
post_data = {
|
||||||
|
@ -718,6 +745,15 @@ class MultiParserTests(SimpleTestCase):
|
||||||
with self.subTest(file_name=file_name):
|
with self.subTest(file_name=file_name):
|
||||||
self.assertEqual(parser.sanitize_file_name(file_name), 'hax0rd.txt')
|
self.assertEqual(parser.sanitize_file_name(file_name), 'hax0rd.txt')
|
||||||
|
|
||||||
|
def test_sanitize_invalid_file_name(self):
|
||||||
|
parser = MultiPartParser({
|
||||||
|
'CONTENT_TYPE': 'multipart/form-data; boundary=_foo',
|
||||||
|
'CONTENT_LENGTH': '1',
|
||||||
|
}, StringIO('x'), [], 'utf-8')
|
||||||
|
for file_name in CANDIDATE_INVALID_FILE_NAMES:
|
||||||
|
with self.subTest(file_name=file_name):
|
||||||
|
self.assertIsNone(parser.sanitize_file_name(file_name))
|
||||||
|
|
||||||
def test_rfc2231_parsing(self):
|
def test_rfc2231_parsing(self):
|
||||||
test_data = (
|
test_data = (
|
||||||
(b"Content-Type: application/x-stuff; title*=us-ascii'en-us'This%20is%20%2A%2A%2Afun%2A%2A%2A",
|
(b"Content-Type: application/x-stuff; title*=us-ascii'en-us'This%20is%20%2A%2A%2Afun%2A%2A%2A",
|
||||||
|
|
|
@ -21,10 +21,12 @@ class FileFieldTest(SimpleTestCase):
|
||||||
f.clean(None, '')
|
f.clean(None, '')
|
||||||
self.assertEqual('files/test2.pdf', f.clean(None, 'files/test2.pdf'))
|
self.assertEqual('files/test2.pdf', f.clean(None, 'files/test2.pdf'))
|
||||||
no_file_msg = "'No file was submitted. Check the encoding type on the form.'"
|
no_file_msg = "'No file was submitted. Check the encoding type on the form.'"
|
||||||
|
file = SimpleUploadedFile(None, b'')
|
||||||
|
file._name = ''
|
||||||
with self.assertRaisesMessage(ValidationError, no_file_msg):
|
with self.assertRaisesMessage(ValidationError, no_file_msg):
|
||||||
f.clean(SimpleUploadedFile('', b''))
|
f.clean(file)
|
||||||
with self.assertRaisesMessage(ValidationError, no_file_msg):
|
with self.assertRaisesMessage(ValidationError, no_file_msg):
|
||||||
f.clean(SimpleUploadedFile('', b''), '')
|
f.clean(file, '')
|
||||||
self.assertEqual('files/test3.pdf', f.clean(None, 'files/test3.pdf'))
|
self.assertEqual('files/test3.pdf', f.clean(None, 'files/test3.pdf'))
|
||||||
with self.assertRaisesMessage(ValidationError, no_file_msg):
|
with self.assertRaisesMessage(ValidationError, no_file_msg):
|
||||||
f.clean('some content that is not a file')
|
f.clean('some content that is not a file')
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from django.core.exceptions import SuspiciousFileOperation
|
||||||
from django.test import SimpleTestCase
|
from django.test import SimpleTestCase
|
||||||
from django.utils import text
|
from django.utils import text
|
||||||
from django.utils.functional import lazystr
|
from django.utils.functional import lazystr
|
||||||
|
@ -228,6 +229,13 @@ class TestUtilsText(SimpleTestCase):
|
||||||
filename = "^&'@{}[],$=!-#()%+~_123.txt"
|
filename = "^&'@{}[],$=!-#()%+~_123.txt"
|
||||||
self.assertEqual(text.get_valid_filename(filename), "-_123.txt")
|
self.assertEqual(text.get_valid_filename(filename), "-_123.txt")
|
||||||
self.assertEqual(text.get_valid_filename(lazystr(filename)), "-_123.txt")
|
self.assertEqual(text.get_valid_filename(lazystr(filename)), "-_123.txt")
|
||||||
|
msg = "Could not derive file name from '???'"
|
||||||
|
with self.assertRaisesMessage(SuspiciousFileOperation, msg):
|
||||||
|
text.get_valid_filename('???')
|
||||||
|
# After sanitizing this would yield '..'.
|
||||||
|
msg = "Could not derive file name from '$.$.$'"
|
||||||
|
with self.assertRaisesMessage(SuspiciousFileOperation, msg):
|
||||||
|
text.get_valid_filename('$.$.$')
|
||||||
|
|
||||||
def test_compress_sequence(self):
|
def test_compress_sequence(self):
|
||||||
data = [{'key': i} for i in range(10)]
|
data = [{'key': i} for i in range(10)]
|
||||||
|
|
Loading…
Reference in New Issue