summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNarpat Mali <narpat.mali@windriver.com>2023-10-06 14:13:49 +0000
committerArmin Kuster <akuster808@gmail.com>2023-10-17 08:44:46 -0400
commit6432fee6d04bec8573f1afcc5a9301899d05ac0f (patch)
treec021422e1365712a1db92e1006a7e26127768578
parente2b534cc3a9f178b909c1e15c4b5919c7c0395db (diff)
downloadmeta-openembedded-6432fee6d04bec8573f1afcc5a9301899d05ac0f.tar.gz
python3-gevent: fix CVE-2023-41419
An issue in Gevent Gevent before version 23.9.1 allows a remote attacker to escalate privileges via a crafted script to the WSGIServer component. References: https://nvd.nist.gov/vuln/detail/CVE-2023-41419 https://github.com/advisories/GHSA-x7m3-jprg-wc5g Signed-off-by: Narpat Mali <narpat.mali@windriver.com> Signed-off-by: Armin Kuster <akuster808@gmail.com>
-rw-r--r--meta-python/recipes-devtools/python/python3-gevent/CVE-2023-41419.patch673
-rw-r--r--meta-python/recipes-devtools/python/python3-gevent_21.12.0.bb2
2 files changed, 675 insertions, 0 deletions
diff --git a/meta-python/recipes-devtools/python/python3-gevent/CVE-2023-41419.patch b/meta-python/recipes-devtools/python/python3-gevent/CVE-2023-41419.patch
new file mode 100644
index 0000000000..c92ba876a8
--- /dev/null
+++ b/meta-python/recipes-devtools/python/python3-gevent/CVE-2023-41419.patch
@@ -0,0 +1,673 @@
1From f80ee15e27b67b6fdd101d5f91cf584d19b2b26e Mon Sep 17 00:00:00 2001
2From: Jason Madden <jamadden@gmail.com>
3Date: Fri, 6 Oct 2023 12:41:59 +0000
4Subject: [PATCH] gevent.pywsgi: Much improved handling of chunk trailers.
5 Validation is much stricter to the specification.
6
7Fixes #1989
8
9CVE: CVE-2023-41419
10
11Upstream-Status: Backport [https://github.com/gevent/gevent/commit/2f53c851eaf926767fbac62385615efd4886221c]
12
13Signed-off-by: Narpat Mali <narpat.mali@windriver.com>
14---
15 docs/changes/1989.bugfix | 26 ++++
16 src/gevent/pywsgi.py | 229 ++++++++++++++++++++++++-------
17 src/gevent/subprocess.py | 7 +-
18 src/gevent/testing/testcase.py | 2 +-
19 src/gevent/tests/test__pywsgi.py | 193 ++++++++++++++++++++++++--
20 5 files changed, 390 insertions(+), 67 deletions(-)
21 create mode 100644 docs/changes/1989.bugfix
22
23diff --git a/docs/changes/1989.bugfix b/docs/changes/1989.bugfix
24new file mode 100644
25index 0000000..7ce4a93
26--- /dev/null
27+++ b/docs/changes/1989.bugfix
28@@ -0,0 +1,26 @@
29+Make ``gevent.pywsgi`` comply more closely with the HTTP specification
30+for chunked transfer encoding. In particular, we are much stricter
31+about trailers, and trailers that are invalid (too long or featuring
32+disallowed characters) forcibly close the connection to the client
33+*after* the results have been sent.
34+
35+Trailers otherwise continue to be ignored and are not available to the
36+WSGI application.
37+
38+Previously, carefully crafted invalid trailers in chunked requests on
39+keep-alive connections might appear as two requests to
40+``gevent.pywsgi``. Because this was handled exactly as a normal
41+keep-alive connection with two requests, the WSGI application should
42+handle it normally. However, if you were counting on some upstream
43+server to filter incoming requests based on paths or header fields,
44+and the upstream server simply passed trailers through without
45+validating them, then this embedded second request would bypass those
46+checks. (If the upstream server validated that the trailers meet the
47+HTTP specification, this could not occur, because characters that are
48+required in an HTTP request, like a space, are not allowed in
49+trailers.) CVE-2023-41419 was reserved for this.
50+
51+Our thanks to the original reporters, Keran Mu
52+(mkr22@mails.tsinghua.edu.cn) and Jianjun Chen
53+(jianjun@tsinghua.edu.cn), from Tsinghua University and Zhongguancun
54+Laboratory.
55diff --git a/src/gevent/pywsgi.py b/src/gevent/pywsgi.py
56index 0ebe095..078398a 100644
57--- a/src/gevent/pywsgi.py
58+++ b/src/gevent/pywsgi.py
59@@ -1,13 +1,28 @@
60 # Copyright (c) 2005-2009, eventlet contributors
61 # Copyright (c) 2009-2018, gevent contributors
62 """
63-A pure-Python, gevent-friendly WSGI server.
64+A pure-Python, gevent-friendly WSGI server implementing HTTP/1.1.
65
66 The server is provided in :class:`WSGIServer`, but most of the actual
67 WSGI work is handled by :class:`WSGIHandler` --- a new instance is
68 created for each request. The server can be customized to use
69 different subclasses of :class:`WSGIHandler`.
70
71+.. important::
72+ This server is intended primarily for development and testing, and
73+ secondarily for other "safe" scenarios where it will not be exposed to
74+ potentially malicious input. The code has not been security audited,
75+ and is not intended for direct exposure to the public Internet. For production
76+ usage on the Internet, either choose a production-strength server such as
77+ gunicorn, or put a reverse proxy between gevent and the Internet.
78+.. versionchanged:: NEXT
79+ Complies more closely with the HTTP specification for chunked transfer encoding.
80+ In particular, we are much stricter about trailers, and trailers that
81+ are invalid (too long or featuring disallowed characters) forcibly close
82+ the connection to the client *after* the results have been sent.
83+ Trailers otherwise continue to be ignored and are not available to the
84+ WSGI application.
85+
86 """
87 from __future__ import absolute_import
88
89@@ -22,10 +37,7 @@ import time
90 import traceback
91 from datetime import datetime
92
93-try:
94- from urllib import unquote
95-except ImportError:
96- from urllib.parse import unquote # python 2 pylint:disable=import-error,no-name-in-module
97+from urllib.parse import unquote
98
99 from gevent import socket
100 import gevent
101@@ -53,29 +65,52 @@ __all__ = [
102
103 MAX_REQUEST_LINE = 8192
104 # Weekday and month names for HTTP date/time formatting; always English!
105-_WEEKDAYNAME = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
106-_MONTHNAME = [None, # Dummy so we can use 1-based month numbers
107+_WEEKDAYNAME = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")
108+_MONTHNAME = (None, # Dummy so we can use 1-based month numbers
109 "Jan", "Feb", "Mar", "Apr", "May", "Jun",
110- "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
111+ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec")
112
113 # The contents of the "HEX" grammar rule for HTTP, upper and lowercase A-F plus digits,
114 # in byte form for comparing to the network.
115 _HEX = string.hexdigits.encode('ascii')
116
117+# The characters allowed in "token" rules.
118+
119+# token = 1*tchar
120+# tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
121+# / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
122+# / DIGIT / ALPHA
123+# ; any VCHAR, except delimiters
124+# ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
125+_ALLOWED_TOKEN_CHARS = frozenset(
126+ # Remember we have to be careful because bytestrings
127+ # inexplicably iterate as integers, which are not equal to bytes.
128+
129+ # explicit chars then DIGIT
130+ (c.encode('ascii') for c in "!#$%&'*+-.^_`|~0123456789")
131+ # Then we add ALPHA
132+) | {c.encode('ascii') for c in string.ascii_letters}
133+assert b'A' in _ALLOWED_TOKEN_CHARS
134+
135+
136 # Errors
137 _ERRORS = {}
138 _INTERNAL_ERROR_STATUS = '500 Internal Server Error'
139 _INTERNAL_ERROR_BODY = b'Internal Server Error'
140-_INTERNAL_ERROR_HEADERS = [('Content-Type', 'text/plain'),
141- ('Connection', 'close'),
142- ('Content-Length', str(len(_INTERNAL_ERROR_BODY)))]
143+_INTERNAL_ERROR_HEADERS = (
144+ ('Content-Type', 'text/plain'),
145+ ('Connection', 'close'),
146+ ('Content-Length', str(len(_INTERNAL_ERROR_BODY)))
147+)
148 _ERRORS[500] = (_INTERNAL_ERROR_STATUS, _INTERNAL_ERROR_HEADERS, _INTERNAL_ERROR_BODY)
149
150 _BAD_REQUEST_STATUS = '400 Bad Request'
151 _BAD_REQUEST_BODY = ''
152-_BAD_REQUEST_HEADERS = [('Content-Type', 'text/plain'),
153- ('Connection', 'close'),
154- ('Content-Length', str(len(_BAD_REQUEST_BODY)))]
155+_BAD_REQUEST_HEADERS = (
156+ ('Content-Type', 'text/plain'),
157+ ('Connection', 'close'),
158+ ('Content-Length', str(len(_BAD_REQUEST_BODY)))
159+)
160 _ERRORS[400] = (_BAD_REQUEST_STATUS, _BAD_REQUEST_HEADERS, _BAD_REQUEST_BODY)
161
162 _REQUEST_TOO_LONG_RESPONSE = b"HTTP/1.1 414 Request URI Too Long\r\nConnection: close\r\nContent-length: 0\r\n\r\n"
163@@ -204,23 +239,32 @@ class Input(object):
164 # Read and return the next integer chunk length. If no
165 # chunk length can be read, raises _InvalidClientInput.
166
167- # Here's the production for a chunk:
168- # (http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html)
169- # chunk = chunk-size [ chunk-extension ] CRLF
170- # chunk-data CRLF
171- # chunk-size = 1*HEX
172- # chunk-extension= *( ";" chunk-ext-name [ "=" chunk-ext-val ] )
173- # chunk-ext-name = token
174- # chunk-ext-val = token | quoted-string
175-
176- # To cope with malicious or broken clients that fail to send valid
177- # chunk lines, the strategy is to read character by character until we either reach
178- # a ; or newline. If at any time we read a non-HEX digit, we bail. If we hit a
179- # ;, indicating an chunk-extension, we'll read up to the next
180- # MAX_REQUEST_LINE characters
181- # looking for the CRLF, and if we don't find it, we bail. If we read more than 16 hex characters,
182- # (the number needed to represent a 64-bit chunk size), we bail (this protects us from
183- # a client that sends an infinite stream of `F`, for example).
184+ # Here's the production for a chunk (actually the whole body):
185+ # (https://www.rfc-editor.org/rfc/rfc7230#section-4.1)
186+
187+ # chunked-body = *chunk
188+ # last-chunk
189+ # trailer-part
190+ # CRLF
191+ #
192+ # chunk = chunk-size [ chunk-ext ] CRLF
193+ # chunk-data CRLF
194+ # chunk-size = 1*HEXDIG
195+ # last-chunk = 1*("0") [ chunk-ext ] CRLF
196+ # trailer-part = *( header-field CRLF )
197+ # chunk-data = 1*OCTET ; a sequence of chunk-size octets
198+
199+ # To cope with malicious or broken clients that fail to send
200+ # valid chunk lines, the strategy is to read character by
201+ # character until we either reach a ; or newline. If at any
202+ # time we read a non-HEX digit, we bail. If we hit a ;,
203+ # indicating an chunk-extension, we'll read up to the next
204+ # MAX_REQUEST_LINE characters ("A server ought to limit the
205+ # total length of chunk extensions received") looking for the
206+ # CRLF, and if we don't find it, we bail. If we read more than
207+ # 16 hex characters, (the number needed to represent a 64-bit
208+ # chunk size), we bail (this protects us from a client that
209+ # sends an infinite stream of `F`, for example).
210
211 buf = BytesIO()
212 while 1:
213@@ -228,16 +272,20 @@ class Input(object):
214 if not char:
215 self._chunked_input_error = True
216 raise _InvalidClientInput("EOF before chunk end reached")
217- if char == b'\r':
218- break
219- if char == b';':
220+
221+ if char in (
222+ b'\r', # Beginning EOL
223+ b';', # Beginning extension
224+ ):
225 break
226
227- if char not in _HEX:
228+ if char not in _HEX: # Invalid data.
229 self._chunked_input_error = True
230 raise _InvalidClientInput("Non-hex data", char)
231+
232 buf.write(char)
233- if buf.tell() > 16:
234+
235+ if buf.tell() > 16: # Too many hex bytes
236 self._chunked_input_error = True
237 raise _InvalidClientInput("Chunk-size too large.")
238
239@@ -257,11 +305,72 @@ class Input(object):
240 if char == b'\r':
241 # We either got here from the main loop or from the
242 # end of an extension
243+ self.__read_chunk_size_crlf(rfile, newline_only=True)
244+ result = int(buf.getvalue(), 16)
245+ if result == 0:
246+ # The only time a chunk size of zero is allowed is the final
247+ # chunk. It is either followed by another \r\n, or some trailers
248+ # which are then followed by \r\n.
249+ while self.__read_chunk_trailer(rfile):
250+ pass
251+ return result
252+
253+ # Trailers have the following production (they are a header-field followed by CRLF)
254+ # See above for the definition of "token".
255+ #
256+ # header-field = field-name ":" OWS field-value OWS
257+ # field-name = token
258+ # field-value = *( field-content / obs-fold )
259+ # field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
260+ # field-vchar = VCHAR / obs-text
261+ # obs-fold = CRLF 1*( SP / HTAB )
262+ # ; obsolete line folding
263+ # ; see Section 3.2.4
264+
265+
266+ def __read_chunk_trailer(self, rfile, ):
267+ # With rfile positioned just after a \r\n, read a trailer line.
268+ # Return a true value if a non-empty trailer was read, and
269+ # return false if an empty trailer was read (meaning the trailers are
270+ # done).
271+ # If a single line exceeds the MAX_REQUEST_LINE, raise an exception.
272+ # If the field-name portion contains invalid characters, raise an exception.
273+
274+ i = 0
275+ empty = True
276+ seen_field_name = False
277+ while i < MAX_REQUEST_LINE:
278+ char = rfile.read(1)
279+ if char == b'\r':
280+ # Either read the next \n or raise an error.
281+ self.__read_chunk_size_crlf(rfile, newline_only=True)
282+ break
283+ # Not a \r, so we are NOT an empty chunk.
284+ empty = False
285+ if char == b':' and i > 0:
286+ # We're ending the field-name part; stop validating characters.
287+ # Unless : was the first character...
288+ seen_field_name = True
289+ if not seen_field_name and char not in _ALLOWED_TOKEN_CHARS:
290+ raise _InvalidClientInput('Invalid token character: %r' % (char,))
291+ i += 1
292+ else:
293+ # We read too much
294+ self._chunked_input_error = True
295+ raise _InvalidClientInput("Too large chunk trailer")
296+ return not empty
297+
298+ def __read_chunk_size_crlf(self, rfile, newline_only=False):
299+ # Also for safety, correctly verify that we get \r\n when expected.
300+ if not newline_only:
301 char = rfile.read(1)
302- if char != b'\n':
303+ if char != b'\r':
304 self._chunked_input_error = True
305- raise _InvalidClientInput("Line didn't end in CRLF")
306- return int(buf.getvalue(), 16)
307+ raise _InvalidClientInput("Line didn't end in CRLF: %r" % (char,))
308+ char = rfile.read(1)
309+ if char != b'\n':
310+ self._chunked_input_error = True
311+ raise _InvalidClientInput("Line didn't end in LF: %r" % (char,))
312
313 def _chunked_read(self, length=None, use_readline=False):
314 # pylint:disable=too-many-branches
315@@ -294,7 +403,7 @@ class Input(object):
316
317 self.position += datalen
318 if self.chunk_length == self.position:
319- rfile.readline()
320+ self.__read_chunk_size_crlf(rfile)
321
322 if length is not None:
323 length -= datalen
324@@ -307,9 +416,9 @@ class Input(object):
325 # determine the next size to read
326 self.chunk_length = self.__read_chunk_length(rfile)
327 self.position = 0
328- if self.chunk_length == 0:
329- # Last chunk. Terminates with a CRLF.
330- rfile.readline()
331+ # If chunk_length was 0, we already read any trailers and
332+ # validated that we have ended with \r\n\r\n.
333+
334 return b''.join(response)
335
336 def read(self, length=None):
337@@ -532,7 +641,8 @@ class WSGIHandler(object):
338 elif len(words) == 2:
339 self.command, self.path = words
340 if self.command != "GET":
341- raise _InvalidClientRequest('Expected GET method: %r' % (raw_requestline,))
342+ raise _InvalidClientRequest('Expected GET method; Got command=%r; path=%r; raw=%r' % (
343+ self.command, self.path, raw_requestline,))
344 self.request_version = "HTTP/0.9"
345 # QQQ I'm pretty sure we can drop support for HTTP/0.9
346 else:
347@@ -1000,14 +1110,28 @@ class WSGIHandler(object):
348 finally:
349 try:
350 self.wsgi_input._discard()
351- except (socket.error, IOError):
352- # Don't let exceptions during discarding
353+ except _InvalidClientInput:
354+ # This one is deliberately raised to the outer
355+ # scope, because, with the incoming stream in some bad state,
356+ # we can't be sure we can synchronize and properly parse the next
357+ # request.
358+ raise
359+ except socket.error
360+ # Don't let socket exceptions during discarding
361 # input override any exception that may have been
362 # raised by the application, such as our own _InvalidClientInput.
363 # In the general case, these aren't even worth logging (see the comment
364 # just below)
365 pass
366- except _InvalidClientInput:
367+ except _InvalidClientInput as ex:
368+ # DO log this one because:
369+ # - Some of the data may have been read and acted on by the
370+ # application;
371+ # - The response may or may not have been sent;
372+ # - It's likely that the client is bad, or malicious, and
373+ # users might wish to take steps to block the client.
374+ self._handle_client_error(ex)
375+ self.close_connection = True
376 self._send_error_response_if_possible(400)
377 except socket.error as ex:
378 if ex.args[0] in self.ignored_socket_errors:
379@@ -1054,17 +1178,22 @@ class WSGIHandler(object):
380 def _handle_client_error(self, ex):
381 # Called for invalid client input
382 # Returns the appropriate error response.
383- if not isinstance(ex, ValueError):
384+ if not isinstance(ex, (ValueError, _InvalidClientInput)):
385 # XXX: Why not self._log_error to send it through the loop's
386 # handle_error method?
387+ # _InvalidClientRequest is a ValueError; _InvalidClientInput is an IOError.
388 traceback.print_exc()
389 if isinstance(ex, _InvalidClientRequest):
390 # No formatting needed, that's already been handled. In fact, because the
391 # formatted message contains user input, it might have a % in it, and attempting
392 # to format that with no arguments would be an error.
393- self.log_error(ex.formatted_message)
394+ # However, the error messages do not include the requesting IP
395+ # necessarily, so we do add that.
396+ self.log_error('(from %s) %s', self.client_address, ex.formatted_message)
397 else:
398- self.log_error('Invalid request: %s', str(ex) or ex.__class__.__name__)
399+ self.log_error('Invalid request (from %s): %s',
400+ self.client_address,
401+ str(ex) or ex.__class__.__name__)
402 return ('400', _BAD_REQUEST_RESPONSE)
403
404 def _headers(self):
405diff --git a/src/gevent/subprocess.py b/src/gevent/subprocess.py
406index 38c9bd3..8a8ccad 100644
407--- a/src/gevent/subprocess.py
408+++ b/src/gevent/subprocess.py
409@@ -352,10 +352,11 @@ def check_output(*popenargs, **kwargs):
410
411 To capture standard error in the result, use ``stderr=STDOUT``::
412
413- >>> print(check_output(["/bin/sh", "-c",
414+ >>> output = check_output(["/bin/sh", "-c",
415 ... "ls -l non_existent_file ; exit 0"],
416- ... stderr=STDOUT).decode('ascii').strip())
417- ls: non_existent_file: No such file or directory
418+ ... stderr=STDOUT).decode('ascii').strip()
419+ >>> print(output.rsplit(':', 1)[1].strip())
420+ No such file or directory
421
422 There is an additional optional argument, "input", allowing you to
423 pass a string to the subprocess's stdin. If you use this argument
424diff --git a/src/gevent/testing/testcase.py b/src/gevent/testing/testcase.py
425index cd5db80..aa86dcf 100644
426--- a/src/gevent/testing/testcase.py
427+++ b/src/gevent/testing/testcase.py
428@@ -225,7 +225,7 @@ class TestCaseMetaClass(type):
429 classDict.pop(key)
430 # XXX: When did we stop doing this?
431 #value = wrap_switch_count_check(value)
432- value = _wrap_timeout(timeout, value)
433+ #value = _wrap_timeout(timeout, value)
434 error_fatal = getattr(value, 'error_fatal', error_fatal)
435 if error_fatal:
436 value = errorhandler.wrap_error_fatal(value)
437diff --git a/src/gevent/tests/test__pywsgi.py b/src/gevent/tests/test__pywsgi.py
438index d2125a8..d46030b 100644
439--- a/src/gevent/tests/test__pywsgi.py
440+++ b/src/gevent/tests/test__pywsgi.py
441@@ -25,21 +25,11 @@ from gevent import monkey
442 monkey.patch_all()
443
444 from contextlib import contextmanager
445-try:
446- from urllib.parse import parse_qs
447-except ImportError:
448- # Python 2
449- from urlparse import parse_qs
450+from urllib.parse import parse_qs
451 import os
452 import sys
453-try:
454- # On Python 2, we want the C-optimized version if
455- # available; it has different corner-case behaviour than
456- # the Python implementation, and it used by socket.makefile
457- # by default.
458- from cStringIO import StringIO
459-except ImportError:
460- from io import BytesIO as StringIO
461+from io import BytesIO as StringIO
462+
463 import weakref
464 import unittest
465 from wsgiref.validate import validator
466@@ -156,6 +146,10 @@ class Response(object):
467 @classmethod
468 def read(cls, fd, code=200, reason='default', version='1.1',
469 body=None, chunks=None, content_length=None):
470+ """
471+ Read an HTTP response, optionally perform assertions,
472+ and return the Response object.
473+ """
474 # pylint:disable=too-many-branches
475 _status_line, headers = read_headers(fd)
476 self = cls(_status_line, headers)
477@@ -716,7 +710,14 @@ class TestNegativeReadline(TestCase):
478
479 class TestChunkedPost(TestCase):
480
481+ calls = 0
482+
483+ def setUp(self):
484+ super().setUp()
485+ self.calls = 0
486+
487 def application(self, env, start_response):
488+ self.calls += 1
489 self.assertTrue(env.get('wsgi.input_terminated'))
490 start_response('200 OK', [('Content-Type', 'text/plain')])
491 if env['PATH_INFO'] == '/a':
492@@ -730,6 +731,8 @@ class TestChunkedPost(TestCase):
493 if env['PATH_INFO'] == '/c':
494 return list(iter(lambda: env['wsgi.input'].read(1), b''))
495
496+ return [b'We should not get here', env['PATH_INFO'].encode('ascii')]
497+
498 def test_014_chunked_post(self):
499 data = (b'POST /a HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n'
500 b'Transfer-Encoding: chunked\r\n\r\n'
501@@ -797,6 +800,170 @@ class TestChunkedPost(TestCase):
502 fd.write(data)
503 read_http(fd, code=400)
504
505+ def test_trailers_keepalive_ignored(self):
506+ # Trailers after a chunk are ignored.
507+ data = (
508+ b'POST /a HTTP/1.1\r\n'
509+ b'Host: localhost\r\n'
510+ b'Connection: keep-alive\r\n'
511+ b'Transfer-Encoding: chunked\r\n'
512+ b'\r\n'
513+ b'2\r\noh\r\n'
514+ b'4\r\n hai\r\n'
515+ b'0\r\n' # last-chunk
516+ # Normally the final CRLF would go here, but if you put in a
517+ # trailer, it doesn't.
518+ b'trailer1: value1\r\n'
519+ b'trailer2: value2\r\n'
520+ b'\r\n' # Really terminate the chunk.
521+ b'POST /a HTTP/1.1\r\n'
522+ b'Host: localhost\r\n'
523+ b'Connection: close\r\n'
524+ b'Transfer-Encoding: chunked\r\n'
525+ b'\r\n'
526+ b'2\r\noh\r\n'
527+ b'4\r\n bye\r\n'
528+ b'0\r\n' # last-chunk
529+ )
530+ with self.makefile() as fd:
531+ fd.write(data)
532+ read_http(fd, body='oh hai')
533+ read_http(fd, body='oh bye')
534+
535+ self.assertEqual(self.calls, 2)
536+
537+ def test_trailers_too_long(self):
538+ # Trailers after a chunk are ignored.
539+ data = (
540+ b'POST /a HTTP/1.1\r\n'
541+ b'Host: localhost\r\n'
542+ b'Connection: keep-alive\r\n'
543+ b'Transfer-Encoding: chunked\r\n'
544+ b'\r\n'
545+ b'2\r\noh\r\n'
546+ b'4\r\n hai\r\n'
547+ b'0\r\n' # last-chunk
548+ # Normally the final CRLF would go here, but if you put in a
549+ # trailer, it doesn't.
550+ b'trailer2: value2' # not lack of \r\n
551+ )
552+ data += b't' * pywsgi.MAX_REQUEST_LINE
553+ # No termination, because we detect the trailer as being too
554+ # long and abort the connection.
555+ with self.makefile() as fd:
556+ fd.write(data)
557+ read_http(fd, body='oh hai')
558+ with self.assertRaises(ConnectionClosed):
559+ read_http(fd, body='oh bye')
560+
561+ def test_trailers_request_smuggling_missing_last_chunk_keep_alive(self):
562+ # When something that looks like a request line comes in the trailer
563+ # as the first line, immediately after an invalid last chunk.
564+ # We detect this and abort the connection, because the
565+ # whitespace in the GET line isn't a legal part of a trailer.
566+ # If we didn't abort the connection, then, because we specified
567+ # keep-alive, the server would be hanging around waiting for more input.
568+ data = (
569+ b'POST /a HTTP/1.1\r\n'
570+ b'Host: localhost\r\n'
571+ b'Connection: keep-alive\r\n'
572+ b'Transfer-Encoding: chunked\r\n'
573+ b'\r\n'
574+ b'2\r\noh\r\n'
575+ b'4\r\n hai\r\n'
576+ b'0' # last-chunk, but missing the \r\n
577+ # Normally the final CRLF would go here, but if you put in a
578+ # trailer, it doesn't.
579+ # b'\r\n'
580+ b'GET /path2?a=:123 HTTP/1.1\r\n'
581+ b'Host: a.com\r\n'
582+ b'Connection: close\r\n'
583+ b'\r\n'
584+ )
585+ with self.makefile() as fd:
586+ fd.write(data)
587+ read_http(fd, body='oh hai')
588+ with self.assertRaises(ConnectionClosed):
589+ read_http(fd)
590+
591+ self.assertEqual(self.calls, 1)
592+
593+ def test_trailers_request_smuggling_missing_last_chunk_close(self):
594+ # Same as the above, except the trailers are actually valid
595+ # and since we ask to close the connection we don't get stuck
596+ # waiting for more input.
597+ data = (
598+ b'POST /a HTTP/1.1\r\n'
599+ b'Host: localhost\r\n'
600+ b'Connection: close\r\n'
601+ b'Transfer-Encoding: chunked\r\n'
602+ b'\r\n'
603+ b'2\r\noh\r\n'
604+ b'4\r\n hai\r\n'
605+ b'0\r\n' # last-chunk
606+ # Normally the final CRLF would go here, but if you put in a
607+ # trailer, it doesn't.
608+ # b'\r\n'
609+ b'GETpath2a:123 HTTP/1.1\r\n'
610+ b'Host: a.com\r\n'
611+ b'Connection: close\r\n'
612+ b'\r\n'
613+ )
614+ with self.makefile() as fd:
615+ fd.write(data)
616+ read_http(fd, body='oh hai')
617+ with self.assertRaises(ConnectionClosed):
618+ read_http(fd)
619+
620+ def test_trailers_request_smuggling_header_first(self):
621+ # When something that looks like a header comes in the first line.
622+ data = (
623+ b'POST /a HTTP/1.1\r\n'
624+ b'Host: localhost\r\n'
625+ b'Connection: keep-alive\r\n'
626+ b'Transfer-Encoding: chunked\r\n'
627+ b'\r\n'
628+ b'2\r\noh\r\n'
629+ b'4\r\n hai\r\n'
630+ b'0\r\n' # last-chunk, but only one CRLF
631+ b'Header: value\r\n'
632+ b'GET /path2?a=:123 HTTP/1.1\r\n'
633+ b'Host: a.com\r\n'
634+ b'Connection: close\r\n'
635+ b'\r\n'
636+ )
637+ with self.makefile() as fd:
638+ fd.write(data)
639+ read_http(fd, body='oh hai')
640+ with self.assertRaises(ConnectionClosed):
641+ read_http(fd, code=400)
642+
643+ self.assertEqual(self.calls, 1)
644+
645+ def test_trailers_request_smuggling_request_terminates_then_header(self):
646+ data = (
647+ b'POST /a HTTP/1.1\r\n'
648+ b'Host: localhost\r\n'
649+ b'Connection: keep-alive\r\n'
650+ b'Transfer-Encoding: chunked\r\n'
651+ b'\r\n'
652+ b'2\r\noh\r\n'
653+ b'4\r\n hai\r\n'
654+ b'0\r\n' # last-chunk
655+ b'\r\n'
656+ b'Header: value'
657+ b'GET /path2?a=:123 HTTP/1.1\r\n'
658+ b'Host: a.com\r\n'
659+ b'Connection: close\r\n'
660+ b'\r\n'
661+ )
662+ with self.makefile() as fd:
663+ fd.write(data)
664+ read_http(fd, body='oh hai')
665+ read_http(fd, code=400)
666+
667+ self.assertEqual(self.calls, 1)
668+
669
670 class TestUseWrite(TestCase):
671
672--
6732.40.0
diff --git a/meta-python/recipes-devtools/python/python3-gevent_21.12.0.bb b/meta-python/recipes-devtools/python/python3-gevent_21.12.0.bb
index 9efeec4d9f..fd6b0f531a 100644
--- a/meta-python/recipes-devtools/python/python3-gevent_21.12.0.bb
+++ b/meta-python/recipes-devtools/python/python3-gevent_21.12.0.bb
@@ -13,6 +13,8 @@ RDEPENDS:${PN} = "${PYTHON_PN}-greenlet \
13 13
14SRC_URI[sha256sum] = "f48b64578c367b91fa793bf8eaaaf4995cb93c8bc45860e473bf868070ad094e" 14SRC_URI[sha256sum] = "f48b64578c367b91fa793bf8eaaaf4995cb93c8bc45860e473bf868070ad094e"
15 15
16SRC_URI += "file://CVE-2023-41419.patch"
17
16inherit pypi setuptools3 18inherit pypi setuptools3
17 19
18# Don't embed libraries, link to the system instead 20# Don't embed libraries, link to the system instead