diff options
Diffstat (limited to 'meta-python/recipes-devtools/python/python3-django-2.2.16/CVE-2021-28658.patch')
| -rw-r--r-- | meta-python/recipes-devtools/python/python3-django-2.2.16/CVE-2021-28658.patch | 289 |
1 files changed, 0 insertions, 289 deletions
diff --git a/meta-python/recipes-devtools/python/python3-django-2.2.16/CVE-2021-28658.patch b/meta-python/recipes-devtools/python/python3-django-2.2.16/CVE-2021-28658.patch deleted file mode 100644 index 325aa00420..0000000000 --- a/meta-python/recipes-devtools/python/python3-django-2.2.16/CVE-2021-28658.patch +++ /dev/null | |||
| @@ -1,289 +0,0 @@ | |||
| 1 | From 4036d62bda0e9e9f6172943794b744a454ca49c2 Mon Sep 17 00:00:00 2001 | ||
| 2 | From: Mariusz Felisiak <felisiak.mariusz@gmail.com> | ||
| 3 | Date: Tue, 16 Mar 2021 10:19:00 +0100 | ||
| 4 | Subject: [PATCH] Fixed CVE-2021-28658 -- Fixed potential directory-traversal | ||
| 5 | via uploaded files. | ||
| 6 | |||
| 7 | Thanks Claude Paroz for the initial patch. | ||
| 8 | Thanks Dennis Brinkrolf for the report. | ||
| 9 | |||
| 10 | Backport of d4d800ca1addc4141e03c5440a849bb64d1582cd from main. | ||
| 11 | |||
| 12 | Upstream-Status: Backport | ||
| 13 | CVE: CVE-2021-28658 | ||
| 14 | |||
| 15 | Reference to upstream patch: | ||
| 16 | [https://github.com/django/django/commit/4036d62bda0e9e9f6172943794b744a454ca49c2] | ||
| 17 | |||
| 18 | [SG: Adapted stable/2.2.x patch for 2.2.16] | ||
| 19 | Signed-off-by: Stefan Ghinea <stefan.ghinea@windriver.com> | ||
| 20 | --- | ||
| 21 | django/http/multipartparser.py | 13 ++++-- | ||
| 22 | docs/releases/2.2.16.txt | 12 +++++ | ||
| 23 | tests/file_uploads/tests.py | 72 ++++++++++++++++++++++------- | ||
| 24 | tests/file_uploads/uploadhandler.py | 31 +++++++++++++ | ||
| 25 | tests/file_uploads/urls.py | 1 + | ||
| 26 | tests/file_uploads/views.py | 12 ++++- | ||
| 27 | 6 files changed, 120 insertions(+), 21 deletions(-) | ||
| 28 | |||
| 29 | diff --git a/django/http/multipartparser.py b/django/http/multipartparser.py | ||
| 30 | index f6f12ca..5a9cca8 100644 | ||
| 31 | --- a/django/http/multipartparser.py | ||
| 32 | +++ b/django/http/multipartparser.py | ||
| 33 | @@ -7,6 +7,7 @@ file upload handlers for processing. | ||
| 34 | import base64 | ||
| 35 | import binascii | ||
| 36 | import cgi | ||
| 37 | +import os | ||
| 38 | from urllib.parse import unquote | ||
| 39 | |||
| 40 | from django.conf import settings | ||
| 41 | @@ -205,7 +206,7 @@ class MultiPartParser: | ||
| 42 | file_name = disposition.get('filename') | ||
| 43 | if file_name: | ||
| 44 | file_name = force_text(file_name, encoding, errors='replace') | ||
| 45 | - file_name = self.IE_sanitize(unescape_entities(file_name)) | ||
| 46 | + file_name = self.sanitize_file_name(file_name) | ||
| 47 | if not file_name: | ||
| 48 | continue | ||
| 49 | |||
| 50 | @@ -293,9 +294,13 @@ class MultiPartParser: | ||
| 51 | self._files.appendlist(force_text(old_field_name, self._encoding, errors='replace'), file_obj) | ||
| 52 | break | ||
| 53 | |||
| 54 | - def IE_sanitize(self, filename): | ||
| 55 | - """Cleanup filename from Internet Explorer full paths.""" | ||
| 56 | - return filename and filename[filename.rfind("\\") + 1:].strip() | ||
| 57 | + def sanitize_file_name(self, file_name): | ||
| 58 | + file_name = unescape_entities(file_name) | ||
| 59 | + # Cleanup Windows-style path separators. | ||
| 60 | + file_name = file_name[file_name.rfind('\\') + 1:].strip() | ||
| 61 | + return os.path.basename(file_name) | ||
| 62 | + | ||
| 63 | + IE_sanitize = sanitize_file_name | ||
| 64 | |||
| 65 | def _close_files(self): | ||
| 66 | # Free up all file handles. | ||
| 67 | diff --git a/docs/releases/2.2.16.txt b/docs/releases/2.2.16.txt | ||
| 68 | index 31231fb..4b7021b 100644 | ||
| 69 | --- a/docs/releases/2.2.16.txt | ||
| 70 | +++ b/docs/releases/2.2.16.txt | ||
| 71 | @@ -2,6 +2,18 @@ | ||
| 72 | Django 2.2.16 release notes | ||
| 73 | =========================== | ||
| 74 | |||
| 75 | +*April 6, 2021* | ||
| 76 | + | ||
| 77 | +Backported from Django 2.2.20 a fix for a security issue. | ||
| 78 | + | ||
| 79 | +CVE-2021-28658: Potential directory-traversal via uploaded files | ||
| 80 | +================================================================ | ||
| 81 | + | ||
| 82 | +``MultiPartParser`` allowed directory-traversal via uploaded files with | ||
| 83 | +suitably crafted file names. | ||
| 84 | + | ||
| 85 | +Built-in upload handlers were not affected by this vulnerability. | ||
| 86 | + | ||
| 87 | *September 1, 2020* | ||
| 88 | |||
| 89 | Django 2.2.16 fixes two security issues and two data loss bugs in 2.2.15. | ||
| 90 | diff --git a/tests/file_uploads/tests.py b/tests/file_uploads/tests.py | ||
| 91 | index ea4976d..2a08d1b 100644 | ||
| 92 | --- a/tests/file_uploads/tests.py | ||
| 93 | +++ b/tests/file_uploads/tests.py | ||
| 94 | @@ -22,6 +22,21 @@ UNICODE_FILENAME = 'test-0123456789_中文_Orléans.jpg' | ||
| 95 | MEDIA_ROOT = sys_tempfile.mkdtemp() | ||
| 96 | UPLOAD_TO = os.path.join(MEDIA_ROOT, 'test_upload') | ||
| 97 | |||
| 98 | +CANDIDATE_TRAVERSAL_FILE_NAMES = [ | ||
| 99 | + '/tmp/hax0rd.txt', # Absolute path, *nix-style. | ||
| 100 | + 'C:\\Windows\\hax0rd.txt', # Absolute path, win-style. | ||
| 101 | + 'C:/Windows/hax0rd.txt', # Absolute path, broken-style. | ||
| 102 | + '\\tmp\\hax0rd.txt', # Absolute path, broken in a different way. | ||
| 103 | + '/tmp\\hax0rd.txt', # Absolute path, broken by mixing. | ||
| 104 | + 'subdir/hax0rd.txt', # Descendant path, *nix-style. | ||
| 105 | + 'subdir\\hax0rd.txt', # Descendant path, win-style. | ||
| 106 | + 'sub/dir\\hax0rd.txt', # Descendant path, mixed. | ||
| 107 | + '../../hax0rd.txt', # Relative path, *nix-style. | ||
| 108 | + '..\\..\\hax0rd.txt', # Relative path, win-style. | ||
| 109 | + '../..\\hax0rd.txt', # Relative path, mixed. | ||
| 110 | + '../hax0rd.txt', # HTML entities. | ||
| 111 | +] | ||
| 112 | + | ||
| 113 | |||
| 114 | @override_settings(MEDIA_ROOT=MEDIA_ROOT, ROOT_URLCONF='file_uploads.urls', MIDDLEWARE=[]) | ||
| 115 | class FileUploadTests(TestCase): | ||
| 116 | @@ -205,22 +220,8 @@ class FileUploadTests(TestCase): | ||
| 117 | # a malicious payload with an invalid file name (containing os.sep or | ||
| 118 | # os.pardir). This similar to what an attacker would need to do when | ||
| 119 | # trying such an attack. | ||
| 120 | - scary_file_names = [ | ||
| 121 | - "/tmp/hax0rd.txt", # Absolute path, *nix-style. | ||
| 122 | - "C:\\Windows\\hax0rd.txt", # Absolute path, win-style. | ||
| 123 | - "C:/Windows/hax0rd.txt", # Absolute path, broken-style. | ||
| 124 | - "\\tmp\\hax0rd.txt", # Absolute path, broken in a different way. | ||
| 125 | - "/tmp\\hax0rd.txt", # Absolute path, broken by mixing. | ||
| 126 | - "subdir/hax0rd.txt", # Descendant path, *nix-style. | ||
| 127 | - "subdir\\hax0rd.txt", # Descendant path, win-style. | ||
| 128 | - "sub/dir\\hax0rd.txt", # Descendant path, mixed. | ||
| 129 | - "../../hax0rd.txt", # Relative path, *nix-style. | ||
| 130 | - "..\\..\\hax0rd.txt", # Relative path, win-style. | ||
| 131 | - "../..\\hax0rd.txt" # Relative path, mixed. | ||
| 132 | - ] | ||
| 133 | - | ||
| 134 | payload = client.FakePayload() | ||
| 135 | - for i, name in enumerate(scary_file_names): | ||
| 136 | + for i, name in enumerate(CANDIDATE_TRAVERSAL_FILE_NAMES): | ||
| 137 | payload.write('\r\n'.join([ | ||
| 138 | '--' + client.BOUNDARY, | ||
| 139 | 'Content-Disposition: form-data; name="file%s"; filename="%s"' % (i, name), | ||
| 140 | @@ -240,7 +241,7 @@ class FileUploadTests(TestCase): | ||
| 141 | response = self.client.request(**r) | ||
| 142 | # The filenames should have been sanitized by the time it got to the view. | ||
| 143 | received = response.json() | ||
| 144 | - for i, name in enumerate(scary_file_names): | ||
| 145 | + for i, name in enumerate(CANDIDATE_TRAVERSAL_FILE_NAMES): | ||
| 146 | got = received["file%s" % i] | ||
| 147 | self.assertEqual(got, "hax0rd.txt") | ||
| 148 | |||
| 149 | @@ -518,6 +519,36 @@ class FileUploadTests(TestCase): | ||
| 150 | # shouldn't differ. | ||
| 151 | self.assertEqual(os.path.basename(obj.testfile.path), 'MiXeD_cAsE.txt') | ||
| 152 | |||
| 153 | + def test_filename_traversal_upload(self): | ||
| 154 | + os.makedirs(UPLOAD_TO, exist_ok=True) | ||
| 155 | + self.addCleanup(shutil.rmtree, MEDIA_ROOT) | ||
| 156 | + file_name = '../test.txt', | ||
| 157 | + payload = client.FakePayload() | ||
| 158 | + payload.write( | ||
| 159 | + '\r\n'.join([ | ||
| 160 | + '--' + client.BOUNDARY, | ||
| 161 | + 'Content-Disposition: form-data; name="my_file"; ' | ||
| 162 | + 'filename="%s";' % file_name, | ||
| 163 | + 'Content-Type: text/plain', | ||
| 164 | + '', | ||
| 165 | + 'file contents.\r\n', | ||
| 166 | + '\r\n--' + client.BOUNDARY + '--\r\n', | ||
| 167 | + ]), | ||
| 168 | + ) | ||
| 169 | + r = { | ||
| 170 | + 'CONTENT_LENGTH': len(payload), | ||
| 171 | + 'CONTENT_TYPE': client.MULTIPART_CONTENT, | ||
| 172 | + 'PATH_INFO': '/upload_traversal/', | ||
| 173 | + 'REQUEST_METHOD': 'POST', | ||
| 174 | + 'wsgi.input': payload, | ||
| 175 | + } | ||
| 176 | + response = self.client.request(**r) | ||
| 177 | + result = response.json() | ||
| 178 | + self.assertEqual(response.status_code, 200) | ||
| 179 | + self.assertEqual(result['file_name'], 'test.txt') | ||
| 180 | + self.assertIs(os.path.exists(os.path.join(MEDIA_ROOT, 'test.txt')), False) | ||
| 181 | + self.assertIs(os.path.exists(os.path.join(UPLOAD_TO, 'test.txt')), True) | ||
| 182 | + | ||
| 183 | |||
| 184 | @override_settings(MEDIA_ROOT=MEDIA_ROOT) | ||
| 185 | class DirectoryCreationTests(SimpleTestCase): | ||
| 186 | @@ -591,6 +622,15 @@ class MultiParserTests(SimpleTestCase): | ||
| 187 | }, StringIO('x'), [], 'utf-8') | ||
| 188 | self.assertEqual(multipart_parser._content_length, 0) | ||
| 189 | |||
| 190 | + def test_sanitize_file_name(self): | ||
| 191 | + parser = MultiPartParser({ | ||
| 192 | + 'CONTENT_TYPE': 'multipart/form-data; boundary=_foo', | ||
| 193 | + 'CONTENT_LENGTH': '1' | ||
| 194 | + }, StringIO('x'), [], 'utf-8') | ||
| 195 | + for file_name in CANDIDATE_TRAVERSAL_FILE_NAMES: | ||
| 196 | + with self.subTest(file_name=file_name): | ||
| 197 | + self.assertEqual(parser.sanitize_file_name(file_name), 'hax0rd.txt') | ||
| 198 | + | ||
| 199 | def test_rfc2231_parsing(self): | ||
| 200 | test_data = ( | ||
| 201 | (b"Content-Type: application/x-stuff; title*=us-ascii'en-us'This%20is%20%2A%2A%2Afun%2A%2A%2A", | ||
| 202 | diff --git a/tests/file_uploads/uploadhandler.py b/tests/file_uploads/uploadhandler.py | ||
| 203 | index 7c6199f..65d70c6 100644 | ||
| 204 | --- a/tests/file_uploads/uploadhandler.py | ||
| 205 | +++ b/tests/file_uploads/uploadhandler.py | ||
| 206 | @@ -1,6 +1,8 @@ | ||
| 207 | """ | ||
| 208 | Upload handlers to test the upload API. | ||
| 209 | """ | ||
| 210 | +import os | ||
| 211 | +from tempfile import NamedTemporaryFile | ||
| 212 | |||
| 213 | from django.core.files.uploadhandler import FileUploadHandler, StopUpload | ||
| 214 | |||
| 215 | @@ -35,3 +37,32 @@ class ErroringUploadHandler(FileUploadHandler): | ||
| 216 | """A handler that raises an exception.""" | ||
| 217 | def receive_data_chunk(self, raw_data, start): | ||
| 218 | raise CustomUploadError("Oops!") | ||
| 219 | + | ||
| 220 | + | ||
| 221 | +class TraversalUploadHandler(FileUploadHandler): | ||
| 222 | + """A handler with potential directory-traversal vulnerability.""" | ||
| 223 | + def __init__(self, request=None): | ||
| 224 | + from .views import UPLOAD_TO | ||
| 225 | + | ||
| 226 | + super().__init__(request) | ||
| 227 | + self.upload_dir = UPLOAD_TO | ||
| 228 | + | ||
| 229 | + def file_complete(self, file_size): | ||
| 230 | + self.file.seek(0) | ||
| 231 | + self.file.size = file_size | ||
| 232 | + with open(os.path.join(self.upload_dir, self.file_name), 'wb') as fp: | ||
| 233 | + fp.write(self.file.read()) | ||
| 234 | + return self.file | ||
| 235 | + | ||
| 236 | + def new_file( | ||
| 237 | + self, field_name, file_name, content_type, content_length, charset=None, | ||
| 238 | + content_type_extra=None, | ||
| 239 | + ): | ||
| 240 | + super().new_file( | ||
| 241 | + file_name, file_name, content_length, content_length, charset, | ||
| 242 | + content_type_extra, | ||
| 243 | + ) | ||
| 244 | + self.file = NamedTemporaryFile(suffix='.upload', dir=self.upload_dir) | ||
| 245 | + | ||
| 246 | + def receive_data_chunk(self, raw_data, start): | ||
| 247 | + self.file.write(raw_data) | ||
| 248 | diff --git a/tests/file_uploads/urls.py b/tests/file_uploads/urls.py | ||
| 249 | index 3e7985d..eaac1da 100644 | ||
| 250 | --- a/tests/file_uploads/urls.py | ||
| 251 | +++ b/tests/file_uploads/urls.py | ||
| 252 | @@ -4,6 +4,7 @@ from . import views | ||
| 253 | |||
| 254 | urlpatterns = [ | ||
| 255 | path('upload/', views.file_upload_view), | ||
| 256 | + path('upload_traversal/', views.file_upload_traversal_view), | ||
| 257 | path('verify/', views.file_upload_view_verify), | ||
| 258 | path('unicode_name/', views.file_upload_unicode_name), | ||
| 259 | path('echo/', views.file_upload_echo), | ||
| 260 | diff --git a/tests/file_uploads/views.py b/tests/file_uploads/views.py | ||
| 261 | index d4947e4..137c6f3 100644 | ||
| 262 | --- a/tests/file_uploads/views.py | ||
| 263 | +++ b/tests/file_uploads/views.py | ||
| 264 | @@ -6,7 +6,9 @@ from django.http import HttpResponse, HttpResponseServerError, JsonResponse | ||
| 265 | |||
| 266 | from .models import FileModel | ||
| 267 | from .tests import UNICODE_FILENAME, UPLOAD_TO | ||
| 268 | -from .uploadhandler import ErroringUploadHandler, QuotaUploadHandler | ||
| 269 | +from .uploadhandler import ( | ||
| 270 | + ErroringUploadHandler, QuotaUploadHandler, TraversalUploadHandler, | ||
| 271 | +) | ||
| 272 | |||
| 273 | |||
| 274 | def file_upload_view(request): | ||
| 275 | @@ -158,3 +160,11 @@ def file_upload_fd_closing(request, access): | ||
| 276 | if access == 't': | ||
| 277 | request.FILES # Trigger file parsing. | ||
| 278 | return HttpResponse('') | ||
| 279 | + | ||
| 280 | + | ||
| 281 | +def file_upload_traversal_view(request): | ||
| 282 | + request.upload_handlers.insert(0, TraversalUploadHandler()) | ||
| 283 | + request.FILES # Trigger file parsing. | ||
| 284 | + return JsonResponse( | ||
| 285 | + {'file_name': request.upload_handlers[0].file_name}, | ||
| 286 | + ) | ||
| 287 | -- | ||
| 288 | 2.17.1 | ||
| 289 | |||
