diff -Nru python-django-1.8.7/debian/changelog python-django-1.8.7/debian/changelog --- python-django-1.8.7/debian/changelog 2021-01-25 12:56:58.000000000 +0000 +++ python-django-1.8.7/debian/changelog 2021-03-30 18:57:56.000000000 +0000 @@ -1,3 +1,14 @@ +python-django (1.8.7-1ubuntu5.15) xenial-security; urgency=medium + + * SECURITY UPDATE: Potential directory-traversal via uploaded files + - debian/patches/CVE-2021-28658.patch: properly sanitize filenames in + django/http/multipartparser.py, tests/file_uploads/tests.py, + tests/file_uploads/uploadhandler.py, tests/file_uploads/urls.py, + tests/file_uploads/views.py. + - CVE-2021-28658 + + -- Marc Deslauriers Tue, 30 Mar 2021 14:57:56 -0400 + python-django (1.8.7-1ubuntu5.14) xenial-security; urgency=medium * SECURITY UPDATE: Potential directory-traversal via archive.extract() diff -Nru python-django-1.8.7/debian/patches/CVE-2021-28658.patch python-django-1.8.7/debian/patches/CVE-2021-28658.patch --- python-django-1.8.7/debian/patches/CVE-2021-28658.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-django-1.8.7/debian/patches/CVE-2021-28658.patch 2021-03-31 11:54:33.000000000 +0000 @@ -0,0 +1,253 @@ +Backport of: + +From cfc6ffc2c948730f98fc1376f02b3b243d3af935 Mon Sep 17 00:00:00 2001 +From: Mariusz Felisiak +Date: Tue, 16 Mar 2021 10:19:00 +0100 +Subject: [PATCH] [2.2.x] Fixed CVE-2021-28658 -- Fixed potential + directory-traversal via uploaded files. + +Thanks Claude Paroz for the initial patch. +Thanks Dennis Brinkrolf for the report. +--- + django/http/multipartparser.py | 13 ++++-- + docs/releases/2.2.20.txt | 15 ++++++ + docs/releases/index.txt | 1 + + tests/file_uploads/tests.py | 72 ++++++++++++++++++++++------- + tests/file_uploads/uploadhandler.py | 31 +++++++++++++ + tests/file_uploads/urls.py | 1 + + tests/file_uploads/views.py | 12 ++++- + 7 files changed, 124 insertions(+), 21 deletions(-) + create mode 100644 docs/releases/2.2.20.txt + +--- a/django/http/multipartparser.py ++++ b/django/http/multipartparser.py +@@ -9,6 +9,7 @@ from __future__ import unicode_literals + import base64 + import binascii + import cgi ++import os + import sys + + from django.conf import settings +@@ -184,7 +185,7 @@ class MultiPartParser(object): + if not file_name: + continue + file_name = force_text(file_name, encoding, errors='replace') +- file_name = self.IE_sanitize(unescape_entities(file_name)) ++ file_name = self.sanitize_file_name(file_name) + + content_type, content_type_extra = meta_data.get('content-type', ('', {})) + content_type = content_type.strip() +@@ -274,9 +275,13 @@ class MultiPartParser(object): + file_obj) + break + +- def IE_sanitize(self, filename): +- """Cleanup filename from Internet Explorer full paths.""" +- return filename and filename[filename.rfind("\\") + 1:].strip() ++ def sanitize_file_name(self, file_name): ++ file_name = unescape_entities(file_name) ++ # Cleanup Windows-style path separators. ++ file_name = file_name[file_name.rfind('\\') + 1:].strip() ++ return os.path.basename(file_name) ++ ++ IE_sanitize = sanitize_file_name + + def _close_files(self): + # Free up all file handles. +--- a/tests/file_uploads/tests.py ++++ b/tests/file_uploads/tests.py +@@ -25,6 +25,21 @@ UNICODE_FILENAME = 'test-0123456789_中æ + MEDIA_ROOT = sys_tempfile.mkdtemp() + UPLOAD_TO = os.path.join(MEDIA_ROOT, 'test_upload') + ++CANDIDATE_TRAVERSAL_FILE_NAMES = [ ++ '/tmp/hax0rd.txt', # Absolute path, *nix-style. ++ 'C:\\Windows\\hax0rd.txt', # Absolute path, win-style. ++ 'C:/Windows/hax0rd.txt', # Absolute path, broken-style. ++ '\\tmp\\hax0rd.txt', # Absolute path, broken in a different way. ++ '/tmp\\hax0rd.txt', # Absolute path, broken by mixing. ++ 'subdir/hax0rd.txt', # Descendant path, *nix-style. ++ 'subdir\\hax0rd.txt', # Descendant path, win-style. ++ 'sub/dir\\hax0rd.txt', # Descendant path, mixed. ++ '../../hax0rd.txt', # Relative path, *nix-style. ++ '..\\..\\hax0rd.txt', # Relative path, win-style. ++ '../..\\hax0rd.txt', # Relative path, mixed. ++ '../hax0rd.txt', # HTML entities. ++] ++ + + @override_settings(MEDIA_ROOT=MEDIA_ROOT, ROOT_URLCONF='file_uploads.urls', MIDDLEWARE_CLASSES=()) + class FileUploadTests(TestCase): +@@ -181,22 +196,8 @@ class FileUploadTests(TestCase): + # a malicious payload with an invalid file name (containing os.sep or + # os.pardir). This similar to what an attacker would need to do when + # trying such an attack. +- scary_file_names = [ +- "/tmp/hax0rd.txt", # Absolute path, *nix-style. +- "C:\\Windows\\hax0rd.txt", # Absolute path, win-style. +- "C:/Windows/hax0rd.txt", # Absolute path, broken-style. +- "\\tmp\\hax0rd.txt", # Absolute path, broken in a different way. +- "/tmp\\hax0rd.txt", # Absolute path, broken by mixing. +- "subdir/hax0rd.txt", # Descendant path, *nix-style. +- "subdir\\hax0rd.txt", # Descendant path, win-style. +- "sub/dir\\hax0rd.txt", # Descendant path, mixed. +- "../../hax0rd.txt", # Relative path, *nix-style. +- "..\\..\\hax0rd.txt", # Relative path, win-style. +- "../..\\hax0rd.txt" # Relative path, mixed. +- ] +- + payload = client.FakePayload() +- for i, name in enumerate(scary_file_names): ++ for i, name in enumerate(CANDIDATE_TRAVERSAL_FILE_NAMES): + payload.write('\r\n'.join([ + '--' + client.BOUNDARY, + 'Content-Disposition: form-data; name="file%s"; filename="%s"' % (i, name), +@@ -217,7 +218,7 @@ class FileUploadTests(TestCase): + + # The filenames should have been sanitized by the time it got to the view. + received = json.loads(response.content.decode('utf-8')) +- for i, name in enumerate(scary_file_names): ++ for i, name in enumerate(CANDIDATE_TRAVERSAL_FILE_NAMES): + got = received["file%s" % i] + self.assertEqual(got, "hax0rd.txt") + +@@ -507,6 +508,37 @@ class FileUploadTests(TestCase): + # shouldn't differ. + self.assertEqual(os.path.basename(obj.testfile.path), 'MiXeD_cAsE.txt') + ++ def test_filename_traversal_upload(self): ++ if not os.path.isdir(UPLOAD_TO): ++ os.makedirs(UPLOAD_TO) ++ self.addCleanup(shutil.rmtree, MEDIA_ROOT) ++ file_name = '../test.txt', ++ payload = client.FakePayload() ++ payload.write( ++ '\r\n'.join([ ++ '--' + client.BOUNDARY, ++ 'Content-Disposition: form-data; name="my_file"; ' ++ 'filename="%s";' % file_name, ++ 'Content-Type: text/plain', ++ '', ++ 'file contents.\r\n', ++ '\r\n--' + client.BOUNDARY + '--\r\n', ++ ]), ++ ) ++ r = { ++ 'CONTENT_LENGTH': len(payload), ++ 'CONTENT_TYPE': client.MULTIPART_CONTENT, ++ 'PATH_INFO': '/upload_traversal/', ++ 'REQUEST_METHOD': 'POST', ++ 'wsgi.input': payload, ++ } ++ response = self.client.request(**r) ++ result = json.loads(response.content.decode('utf-8')) ++ self.assertEqual(response.status_code, 200) ++ self.assertEqual(result['file_name'], 'test.txt') ++ self.assertIs(os.path.exists(os.path.join(MEDIA_ROOT, 'test.txt')), False) ++ self.assertIs(os.path.exists(os.path.join(UPLOAD_TO, 'test.txt')), True) ++ + + @override_settings(MEDIA_ROOT=MEDIA_ROOT) + class DirectoryCreationTests(TestCase): +@@ -563,6 +595,14 @@ class MultiParserTests(unittest.TestCase + 'CONTENT_LENGTH': '1' + }, StringIO('x'), [], 'utf-8') + ++ def test_sanitize_file_name(self): ++ parser = MultiPartParser({ ++ 'CONTENT_TYPE': 'multipart/form-data; boundary=_foo', ++ 'CONTENT_LENGTH': '1' ++ }, StringIO('x'), [], 'utf-8') ++ for file_name in CANDIDATE_TRAVERSAL_FILE_NAMES: ++ self.assertEqual(parser.sanitize_file_name(file_name), 'hax0rd.txt') ++ + def test_rfc2231_parsing(self): + test_data = ( + (b"Content-Type: application/x-stuff; title*=us-ascii'en-us'This%20is%20%2A%2A%2Afun%2A%2A%2A", +--- a/tests/file_uploads/uploadhandler.py ++++ b/tests/file_uploads/uploadhandler.py +@@ -1,6 +1,8 @@ + """ + Upload handlers to test the upload API. + """ ++import os ++from tempfile import NamedTemporaryFile + + from django.core.files.uploadhandler import FileUploadHandler, StopUpload + +@@ -35,3 +37,32 @@ class ErroringUploadHandler(FileUploadHa + """A handler that raises an exception.""" + def receive_data_chunk(self, raw_data, start): + raise CustomUploadError("Oops!") ++ ++ ++class TraversalUploadHandler(FileUploadHandler): ++ """A handler with potential directory-traversal vulnerability.""" ++ def __init__(self, request=None): ++ from .views import UPLOAD_TO ++ ++ super(TraversalUploadHandler, self).__init__(request) ++ self.upload_dir = UPLOAD_TO ++ ++ def file_complete(self, file_size): ++ self.file.seek(0) ++ self.file.size = file_size ++ with open(os.path.join(self.upload_dir, self.file_name), 'wb') as fp: ++ fp.write(self.file.read()) ++ return self.file ++ ++ def new_file( ++ self, field_name, file_name, content_type, content_length, charset=None, ++ content_type_extra=None, ++ ): ++ super(TraversalUploadHandler, self).new_file( ++ file_name, file_name, content_length, content_length, charset, ++ content_type_extra, ++ ) ++ self.file = NamedTemporaryFile(suffix='.upload', dir=self.upload_dir) ++ ++ def receive_data_chunk(self, raw_data, start): ++ self.file.write(raw_data) +--- a/tests/file_uploads/urls.py ++++ b/tests/file_uploads/urls.py +@@ -4,6 +4,7 @@ from . import views + + urlpatterns = [ + url(r'^upload/$', views.file_upload_view), ++ url(r'^upload_traversal/$', views.file_upload_traversal_view), + url(r'^verify/$', views.file_upload_view_verify), + url(r'^unicode_name/$', views.file_upload_unicode_name), + url(r'^echo/$', views.file_upload_echo), +--- a/tests/file_uploads/views.py ++++ b/tests/file_uploads/views.py +@@ -5,13 +5,15 @@ import json + import os + + from django.core.files.uploadedfile import UploadedFile +-from django.http import HttpResponse, HttpResponseServerError ++from django.http import HttpResponse, HttpResponseServerError, JsonResponse + from django.utils import six + from django.utils.encoding import force_bytes, smart_str + + from .models import FileModel + from .tests import UNICODE_FILENAME, UPLOAD_TO +-from .uploadhandler import ErroringUploadHandler, QuotaUploadHandler ++from .uploadhandler import ( ++ ErroringUploadHandler, QuotaUploadHandler, TraversalUploadHandler, ++) + + + def file_upload_view(request): +@@ -163,3 +165,11 @@ def file_upload_fd_closing(request, acce + if access == 't': + request.FILES # Trigger file parsing. + return HttpResponse('') ++ ++ ++def file_upload_traversal_view(request): ++ request.upload_handlers.insert(0, TraversalUploadHandler()) ++ request.FILES # Trigger file parsing. ++ return JsonResponse( ++ {'file_name': request.upload_handlers[0].file_name}, ++ ) diff -Nru python-django-1.8.7/debian/patches/series python-django-1.8.7/debian/patches/series --- python-django-1.8.7/debian/patches/series 2021-01-25 12:56:18.000000000 +0000 +++ python-django-1.8.7/debian/patches/series 2021-03-30 18:56:45.000000000 +0000 @@ -25,3 +25,4 @@ CVE-2020-13254.patch CVE-2020-13596.patch CVE-2021-3281.patch +CVE-2021-28658.patch