diff options
| author | Narpat Mali <narpat.mali@windriver.com> | 2023-10-06 14:13:49 +0000 |
|---|---|---|
| committer | Armin Kuster <akuster808@gmail.com> | 2023-10-17 08:44:46 -0400 |
| commit | 6432fee6d04bec8573f1afcc5a9301899d05ac0f (patch) | |
| tree | c021422e1365712a1db92e1006a7e26127768578 | |
| parent | e2b534cc3a9f178b909c1e15c4b5919c7c0395db (diff) | |
| download | meta-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.patch | 673 | ||||
| -rw-r--r-- | meta-python/recipes-devtools/python/python3-gevent_21.12.0.bb | 2 |
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 @@ | |||
| 1 | From f80ee15e27b67b6fdd101d5f91cf584d19b2b26e Mon Sep 17 00:00:00 2001 | ||
| 2 | From: Jason Madden <jamadden@gmail.com> | ||
| 3 | Date: Fri, 6 Oct 2023 12:41:59 +0000 | ||
| 4 | Subject: [PATCH] gevent.pywsgi: Much improved handling of chunk trailers. | ||
| 5 | Validation is much stricter to the specification. | ||
| 6 | |||
| 7 | Fixes #1989 | ||
| 8 | |||
| 9 | CVE: CVE-2023-41419 | ||
| 10 | |||
| 11 | Upstream-Status: Backport [https://github.com/gevent/gevent/commit/2f53c851eaf926767fbac62385615efd4886221c] | ||
| 12 | |||
| 13 | Signed-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 | |||
| 23 | diff --git a/docs/changes/1989.bugfix b/docs/changes/1989.bugfix | ||
| 24 | new file mode 100644 | ||
| 25 | index 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. | ||
| 55 | diff --git a/src/gevent/pywsgi.py b/src/gevent/pywsgi.py | ||
| 56 | index 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): | ||
| 405 | diff --git a/src/gevent/subprocess.py b/src/gevent/subprocess.py | ||
| 406 | index 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 | ||
| 424 | diff --git a/src/gevent/testing/testcase.py b/src/gevent/testing/testcase.py | ||
| 425 | index 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) | ||
| 437 | diff --git a/src/gevent/tests/test__pywsgi.py b/src/gevent/tests/test__pywsgi.py | ||
| 438 | index 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 | -- | ||
| 673 | 2.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 | ||
| 14 | SRC_URI[sha256sum] = "f48b64578c367b91fa793bf8eaaaf4995cb93c8bc45860e473bf868070ad094e" | 14 | SRC_URI[sha256sum] = "f48b64578c367b91fa793bf8eaaaf4995cb93c8bc45860e473bf868070ad094e" |
| 15 | 15 | ||
| 16 | SRC_URI += "file://CVE-2023-41419.patch" | ||
| 17 | |||
| 16 | inherit pypi setuptools3 | 18 | inherit pypi setuptools3 |
| 17 | 19 | ||
| 18 | # Don't embed libraries, link to the system instead | 20 | # Don't embed libraries, link to the system instead |
