Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
SUSE:SLE-15-SP4:Update
python-gunicorn
CVE-2024-1135.patch
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File CVE-2024-1135.patch of Package python-gunicorn
From 559caf920537ece2ef058e1de5e36af44756bb19 Mon Sep 17 00:00:00 2001 From: "Paul J. Dorn" <pajod@users.noreply.github.com> Date: Wed, 6 Dec 2023 15:30:50 +0100 Subject: [PATCH 01/16] pytest: raise on malformed test fixtures and unbreak test depending on backslash escape --- tests/treq.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) Index: gunicorn-20.1.0/tests/treq.py =================================================================== --- gunicorn-20.1.0.orig/tests/treq.py +++ gunicorn-20.1.0/tests/treq.py @@ -51,7 +51,9 @@ class request(object): with open(self.fname, 'rb') as handle: self.data = handle.read() self.data = self.data.replace(b"\n", b"").replace(b"\\r\\n", b"\r\n") - self.data = self.data.replace(b"\\0", b"\000") + self.data = self.data.replace(b"\\0", b"\000").replace(b"\\n", b"\n").replace(b"\\t", b"\t") + if b"\\" in self.data: + raise AssertionError("Unexpected backslash in test data - only handling HTAB, NUL and CRLF") # Functions for sending data to the parser. # These functions mock out reading from a @@ -246,8 +248,10 @@ class request(object): def check(self, cfg, sender, sizer, matcher): cases = self.expect[:] p = RequestParser(cfg, sender(), None) - for req in p: + parsed_request_idx = -1 + for parsed_request_idx, req in enumerate(p): self.same(req, sizer, matcher, cases.pop(0)) + assert len(self.expect) == parsed_request_idx + 1 assert not cases def same(self, req, sizer, matcher, exp): @@ -262,7 +266,8 @@ class request(object): assert req.trailers == exp.get("trailers", []) -class badrequest(object): +class badrequest: + # FIXME: no good reason why this cannot match what the more extensive mechanism above def __init__(self, fname): self.fname = fname self.name = os.path.basename(fname) @@ -270,7 +275,9 @@ class badrequest(object): with open(self.fname) as handle: self.data = handle.read() self.data = self.data.replace("\n", "").replace("\\r\\n", "\r\n") - self.data = self.data.replace("\\0", "\000") + self.data = self.data.replace("\\0", "\000").replace("\\n", "\n").replace("\\t", "\t") + if "\\" in self.data: + raise AssertionError("Unexpected backslash in test data - only handling HTAB, NUL and CRLF") self.data = self.data.encode('latin1') def send(self): @@ -283,4 +290,6 @@ class badrequest(object): def check(self, cfg): p = RequestParser(cfg, self.send(), None) - next(p) + # must fully consume iterator, otherwise EOF errors could go unnoticed + for _ in p: + pass Index: gunicorn-20.1.0/gunicorn/http/body.py =================================================================== --- gunicorn-20.1.0.orig/gunicorn/http/body.py +++ gunicorn-20.1.0/gunicorn/http/body.py @@ -51,7 +51,7 @@ class ChunkedReader(object): if done: unreader.unread(buf.getvalue()[2:]) return b"" - self.req.trailers = self.req.parse_headers(buf.getvalue()[:idx]) + self.req.trailers = self.req.parse_headers(buf.getvalue()[:idx], from_trailer=True) unreader.unread(buf.getvalue()[idx + 4:]) def parse_chunked(self, unreader): @@ -85,11 +85,13 @@ class ChunkedReader(object): data = buf.getvalue() line, rest_chunk = data[:idx], data[idx + 2:] - chunk_size = line.split(b";", 1)[0].strip() - try: - chunk_size = int(chunk_size, 16) - except ValueError: + # RFC9112 7.1.1: BWS before chunk-ext - but ONLY then + chunk_size, *chunk_ext = line.split(b";", 1) + if chunk_ext: + chunk_size = chunk_size.rstrip(b" \t") + if any(n not in b"0123456789abcdefABCDEF" for n in chunk_size): raise InvalidChunkSize(chunk_size) + chunk_size = int(chunk_size, 16) if chunk_size == 0: try: Index: gunicorn-20.1.0/gunicorn/http/message.py =================================================================== --- gunicorn-20.1.0.orig/gunicorn/http/message.py +++ gunicorn-20.1.0/gunicorn/http/message.py @@ -12,6 +12,7 @@ from gunicorn.http.errors import ( InvalidHeader, InvalidHeaderName, NoMoreData, InvalidRequestLine, InvalidRequestMethod, InvalidHTTPVersion, LimitRequestLine, LimitRequestHeaders, + UnsupportedTransferCoding, ) from gunicorn.http.errors import InvalidProxyLine, ForbiddenProxyRequest from gunicorn.http.errors import InvalidSchemeHeaders @@ -21,9 +22,12 @@ MAX_REQUEST_LINE = 8190 MAX_HEADERS = 32768 DEFAULT_MAX_HEADERFIELD_SIZE = 8190 -HEADER_RE = re.compile(r"[\x00-\x1F\x7F()<>@,;:\[\]={} \t\\\"]") -METH_RE = re.compile(r"[A-Z0-9$-_.]{3,20}") -VERSION_RE = re.compile(r"HTTP/(\d+)\.(\d+)") +# verbosely on purpose, avoid backslash ambiguity +RFC9110_5_6_2_TOKEN_SPECIALS = r"!#$%&'*+-.^_`|~" +TOKEN_RE = re.compile(r"[%s0-9a-zA-Z]+" % (re.escape(RFC9110_5_6_2_TOKEN_SPECIALS))) +METHOD_BADCHAR_RE = re.compile("[a-z#]") +# usually 1.0 or 1.1 - RFC9112 permits restricting to single-digit versions +VERSION_RE = re.compile(r"HTTP/(\d)\.(\d)") class Message(object): @@ -36,6 +40,7 @@ class Message(object): self.trailers = [] self.body = None self.scheme = "https" if cfg.is_ssl else "http" + self.must_close = False # set headers limits self.limit_request_fields = cfg.limit_request_fields @@ -55,22 +60,29 @@ class Message(object): self.unreader.unread(unused) self.set_body_reader() + def force_close(self): + self.must_close = True + def parse(self, unreader): raise NotImplementedError() - def parse_headers(self, data): + def parse_headers(self, data, from_trailer=False): cfg = self.cfg headers = [] - # Split lines on \r\n keeping the \r\n on each line - lines = [bytes_to_str(line) + "\r\n" for line in data.split(b"\r\n")] + # Split lines on \r\n + lines = [bytes_to_str(line) for line in data.split(b"\r\n")] # handle scheme headers scheme_header = False secure_scheme_headers = {} - if ('*' in cfg.forwarded_allow_ips or - not isinstance(self.peer_addr, tuple) - or self.peer_addr[0] in cfg.forwarded_allow_ips): + if from_trailer: + # nonsense. either a request is https from the beginning + # .. or we are just behind a proxy who does not remove conflicting trailers + pass + elif ('*' in cfg.forwarded_allow_ips or + not isinstance(self.peer_addr, tuple) + or self.peer_addr[0] in cfg.forwarded_allow_ips): secure_scheme_headers = cfg.secure_scheme_headers # Parse headers into key/value pairs paying attention @@ -79,30 +91,34 @@ class Message(object): if len(headers) >= self.limit_request_fields: raise LimitRequestHeaders("limit request headers fields") - # Parse initial header name : value pair. + # Parse initial header name: value pair. curr = lines.pop(0) - header_length = len(curr) - if curr.find(":") < 0: - raise InvalidHeader(curr.strip()) + header_length = len(curr) + len("\r\n") + if curr.find(":") <= 0: + raise InvalidHeader(curr) name, value = curr.split(":", 1) if self.cfg.strip_header_spaces: - name = name.rstrip(" \t").upper() - else: - name = name.upper() - if HEADER_RE.search(name): + name = name.rstrip(" \t") + if not TOKEN_RE.fullmatch(name): raise InvalidHeaderName(name) - name, value = name.strip(), [value.lstrip()] + # this is still a dangerous place to do this + # but it is more correct than doing it before the pattern match: + # after we entered Unicode wonderland, 8bits could case-shift into ASCII: + # b"\xDF".decode("latin-1").upper().encode("ascii") == b"SS" + name = name.upper() + + value = [value.lstrip(" \t")] # Consume value continuation lines while lines and lines[0].startswith((" ", "\t")): curr = lines.pop(0) - header_length += len(curr) + header_length += len(curr) + len("\r\n") if header_length > self.limit_request_field_size > 0: raise LimitRequestHeaders("limit request headers " "fields size") - value.append(curr) - value = ''.join(value).rstrip() + value.append(curr.strip("\t ")) + value = " ".join(value) if header_length > self.limit_request_field_size > 0: raise LimitRequestHeaders("limit request headers fields size") @@ -117,6 +133,23 @@ class Message(object): scheme_header = True self.scheme = scheme + # ambiguous mapping allows fooling downstream, e.g. merging non-identical headers: + # X-Forwarded-For: 2001:db8::ha:cc:ed + # X_Forwarded_For: 127.0.0.1,::1 + # HTTP_X_FORWARDED_FOR = 2001:db8::ha:cc:ed,127.0.0.1,::1 + # Only modify after fixing *ALL* header transformations; network to wsgi env + if "_" in name: + if self.cfg.header_map == "dangerous": + # as if we did not know we cannot safely map this + pass + elif self.cfg.header_map == "drop": + # almost as if it never had been there + # but still counts against resource limits + continue + else: + # fail-safe fallthrough: refuse + raise InvalidHeaderName(name) + headers.append((name, value)) return headers @@ -132,9 +165,47 @@ class Message(object): content_length = value elif name == "TRANSFER-ENCODING": if value.lower() == "chunked": + # DANGER: transer codings stack, and stacked chunking is never intended + if chunked: + raise InvalidHeader("TRANSFER-ENCODING", req=self) chunked = True + elif value.lower() == "identity": + # does not do much, could still plausibly desync from what the proxy does + # safe option: nuke it, its never needed + if chunked: + raise InvalidHeader("TRANSFER-ENCODING", req=self) + elif value.lower() == "": + # lacking security review on this case + # offer the option to restore previous behaviour, but refuse by default, for now + self.force_close() + if not self.cfg.tolerate_dangerous_framing: + raise UnsupportedTransferCoding(value) + # DANGER: do not change lightly; ref: request smuggling + # T-E is a list and we *could* support correctly parsing its elements + # .. but that is only safe after getting all the edge cases right + # .. for which no real-world need exists, so best to NOT open that can of worms + else: + self.force_close() + # even if parser is extended, retain this branch: + # the "chunked not last" case remains to be rejected! + raise UnsupportedTransferCoding(value) if chunked: + # two potentially dangerous cases: + # a) CL + TE (TE overrides CL.. only safe if the recipient sees it that way too) + # b) chunked HTTP/1.0 (always faulty) + if self.version < (1, 1): + # framing wonky, see RFC 9112 Section 6.1 + self.force_close() + if not self.cfg.tolerate_dangerous_framing: + raise InvalidHeader("TRANSFER-ENCODING", req=self) + if content_length is not None: + # we cannot be certain the message framing we understood matches proxy intent + # -> whatever happens next, remaining input must not be trusted + self.force_close() + # either processing or rejecting is permitted in RFC 9112 Section 6.1 + if not self.cfg.tolerate_dangerous_framing: + raise InvalidHeader("CONTENT-LENGTH", req=self) self.body = Body(ChunkedReader(self, self.unreader)) elif content_length is not None: try: @@ -150,9 +221,11 @@ class Message(object): self.body = Body(EOFReader(self.unreader)) def should_close(self): + if self.must_close: + return True for (h, v) in self.headers: if h == "CONNECTION": - v = v.lower().strip() + v = v.lower().strip(" \t") if v == "close": return True elif v == "keep-alive": @@ -226,7 +299,7 @@ class Request(Message): self.unreader.unread(data[2:]) return b"" - self.headers = self.parse_headers(data[:idx]) + self.headers = self.parse_headers(data[:idx], from_trailer=False) ret = data[idx + 4:] buf = None @@ -279,7 +352,7 @@ class Request(Message): raise ForbiddenProxyRequest(self.peer_addr[0]) def parse_proxy_protocol(self, line): - bits = line.split() + bits = line.split(" ") if len(bits) != 6: raise InvalidProxyLine(line) @@ -324,14 +397,27 @@ class Request(Message): } def parse_request_line(self, line_bytes): - bits = [bytes_to_str(bit) for bit in line_bytes.split(None, 2)] + bits = [bytes_to_str(bit) for bit in line_bytes.split(b" ", 2)] if len(bits) != 3: raise InvalidRequestLine(bytes_to_str(line_bytes)) - # Method - if not METH_RE.match(bits[0]): - raise InvalidRequestMethod(bits[0]) - self.method = bits[0].upper() + # Method: RFC9110 Section 9 + self.method = bits[0] + + # nonstandard restriction, suitable for all IANA registered methods + # partially enforced in previous gunicorn versions + if not self.cfg.permit_unconventional_http_method: + if METHOD_BADCHAR_RE.search(self.method): + raise InvalidRequestMethod(self.method) + if not 3 <= len(bits[0]) <= 20: + raise InvalidRequestMethod(self.method) + # standard restriction: RFC9110 token + if not TOKEN_RE.fullmatch(self.method): + raise InvalidRequestMethod(self.method) + # nonstandard and dangerous + # methods are merely uppercase by convention, no case-insensitive treatment is intended + if self.cfg.casefold_http_method: + self.method = self.method.upper() # URI self.uri = bits[1] @@ -345,10 +431,14 @@ class Request(Message): self.fragment = parts.fragment or "" # Version - match = VERSION_RE.match(bits[2]) + match = VERSION_RE.fullmatch(bits[2]) if match is None: raise InvalidHTTPVersion(bits[2]) self.version = (int(match.group(1)), int(match.group(2))) + if not (1, 0) <= self.version < (2, 0): + # if ever relaxing this, carefully review Content-Encoding processing + if not self.cfg.permit_unconventional_http_version: + raise InvalidHTTPVersion(self.version) def set_body_reader(self): super().set_body_reader() Index: gunicorn-20.1.0/gunicorn/http/wsgi.py =================================================================== --- gunicorn-20.1.0.orig/gunicorn/http/wsgi.py +++ gunicorn-20.1.0/gunicorn/http/wsgi.py @@ -9,8 +9,8 @@ import os import re import sys -from gunicorn.http.message import HEADER_RE -from gunicorn.http.errors import InvalidHeader, InvalidHeaderName +from gunicorn.http.message import TOKEN_RE +from gunicorn.http.errors import ConfigurationProblem, InvalidHeader, InvalidHeaderName from gunicorn import SERVER_SOFTWARE, SERVER import gunicorn.util as util @@ -18,7 +18,9 @@ import gunicorn.util as util # with sending files in blocks over 2GB. BLKSIZE = 0x3FFFFFFF -HEADER_VALUE_RE = re.compile(r'[\x00-\x1F\x7F]') +# RFC9110 5.5: field-vchar = VCHAR / obs-text +# RFC4234 B.1: VCHAR = 0x21-x07E = printable ASCII +HEADER_VALUE_RE = re.compile(r'[ \t\x21-\x7e\x80-\xff]*') log = logging.getLogger(__name__) @@ -133,6 +135,8 @@ def create(req, sock, client, server, cf environ['CONTENT_LENGTH'] = hdr_value continue + # do not change lightly, this is a common source of security problems + # RFC9110 Section 17.10 discourages ambiguous or incomplete mappings key = 'HTTP_' + hdr_name.replace('-', '_') if key in environ: hdr_value = "%s,%s" % (environ[key], hdr_value) @@ -180,7 +184,11 @@ def create(req, sock, client, server, cf # set the path and script name path_info = req.path if script_name: - path_info = path_info.split(script_name, 1)[1] + if not path_info.startswith(script_name): + raise ConfigurationProblem( + "Request path %r does not start with SCRIPT_NAME %r" % + (path_info, script_name)) + path_info = path_info[len(script_name):] environ['PATH_INFO'] = util.unquote_to_wsgi_str(path_info) environ['SCRIPT_NAME'] = script_name @@ -249,31 +257,32 @@ class Response(object): if not isinstance(name, str): raise TypeError('%r is not a string' % name) - if HEADER_RE.search(name): + if not TOKEN_RE.fullmatch(name): raise InvalidHeaderName('%r' % name) if not isinstance(value, str): raise TypeError('%r is not a string' % value) - if HEADER_VALUE_RE.search(value): + if not HEADER_VALUE_RE.fullmatch(value): raise InvalidHeader('%r' % value) - value = value.strip() - lname = name.lower().strip() + # RFC9110 5.5 + value = value.strip(" \t") + lname = name.lower() if lname == "content-length": self.response_length = int(value) elif util.is_hoppish(name): if lname == "connection": # handle websocket - if value.lower().strip() == "upgrade": + if value.lower() == "upgrade": self.upgrade = True elif lname == "upgrade": - if value.lower().strip() == "websocket": - self.headers.append((name.strip(), value)) + if value.lower() == "websocket": + self.headers.append((name, value)) # ignore hopbyhop headers continue - self.headers.append((name.strip(), value)) + self.headers.append((name, value)) def is_chunked(self): # Only use chunked responses when the client is Index: gunicorn-20.1.0/tests/requests/invalid/003.http =================================================================== --- gunicorn-20.1.0.orig/tests/requests/invalid/003.http +++ gunicorn-20.1.0/tests/requests/invalid/003.http @@ -1,2 +1,2 @@ --blargh /foo HTTP/1.1\r\n -\r\n \ No newline at end of file +GET\n/\nHTTP/1.1\r\n +\r\n Index: gunicorn-20.1.0/tests/requests/invalid/003.py =================================================================== --- gunicorn-20.1.0.orig/tests/requests/invalid/003.py +++ gunicorn-20.1.0/tests/requests/invalid/003.py @@ -1,2 +1,2 @@ -from gunicorn.http.errors import InvalidRequestMethod -request = InvalidRequestMethod \ No newline at end of file +from gunicorn.http.errors import InvalidRequestLine +request = InvalidRequestLine Index: gunicorn-20.1.0/tests/requests/valid/016.py =================================================================== --- gunicorn-20.1.0.orig/tests/requests/valid/016.py +++ gunicorn-20.1.0/tests/requests/valid/016.py @@ -1,35 +1,35 @@ -certificate = """-----BEGIN CERTIFICATE-----\r\n - MIIFbTCCBFWgAwIBAgICH4cwDQYJKoZIhvcNAQEFBQAwcDELMAkGA1UEBhMCVUsx\r\n - ETAPBgNVBAoTCGVTY2llbmNlMRIwEAYDVQQLEwlBdXRob3JpdHkxCzAJBgNVBAMT\r\n - AkNBMS0wKwYJKoZIhvcNAQkBFh5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMu\r\n - dWswHhcNMDYwNzI3MTQxMzI4WhcNMDcwNzI3MTQxMzI4WjBbMQswCQYDVQQGEwJV\r\n - SzERMA8GA1UEChMIZVNjaWVuY2UxEzARBgNVBAsTCk1hbmNoZXN0ZXIxCzAJBgNV\r\n - BAcTmrsogriqMWLAk1DMRcwFQYDVQQDEw5taWNoYWVsIHBhcmQYJKoZIhvcNAQEB\r\n - BQADggEPADCCAQoCggEBANPEQBgl1IaKdSS1TbhF3hEXSl72G9J+WC/1R64fAcEF\r\n - W51rEyFYiIeZGx/BVzwXbeBoNUK41OK65sxGuflMo5gLflbwJtHBRIEKAfVVp3YR\r\n - gW7cMA/s/XKgL1GEC7rQw8lIZT8RApukCGqOVHSi/F1SiFlPDxuDfmdiNzL31+sL\r\n - 0iwHDdNkGjy5pyBSB8Y79dsSJtCW/iaLB0/n8Sj7HgvvZJ7x0fr+RQjYOUUfrePP\r\n - u2MSpFyf+9BbC/aXgaZuiCvSR+8Snv3xApQY+fULK/xY8h8Ua51iXoQ5jrgu2SqR\r\n - wgA7BUi3G8LFzMBl8FRCDYGUDy7M6QaHXx1ZWIPWNKsCAwEAAaOCAiQwggIgMAwG\r\n - 1UdEwEB/wQCMAAwEQYJYIZIAYb4QgHTTPAQDAgWgMA4GA1UdDwEB/wQEAwID6DAs\r\n - BglghkgBhvhCAQ0EHxYdVUsgZS1TY2llbmNlIFVzZXIgQ2VydGlmaWNhdGUwHQYD\r\n - VR0OBBYEFDTt/sf9PeMaZDHkUIldrDYMNTBZMIGaBgNVHSMEgZIwgY+AFAI4qxGj\r\n - loCLDdMVKwiljjDastqooXSkcjBwMQswCQYDVQQGEwJVSzERMA8GA1UEChMIZVNj\r\n - aWVuY2UxEjAQBgNVBAsTCUF1dGhvcml0eTELMAkGA1UEAxMCQ0ExLTArBgkqhkiG\r\n - 9w0BCQEWHmNhLW9wZXJhdG9yQGdyaWQtc3VwcG9ydC5hYy51a4IBADApBgNVHRIE\r\n - IjAggR5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMudWswGQYDVR0gBBIwEDAO\r\n - BgwrBgEEAdkvAQEBAQYwPQYJYIZIAYb4QgEEBDAWLmh0dHA6Ly9jYS5ncmlkLXN1\r\n - cHBvcnQuYWMudmT4sopwqlBWsvcHViL2NybC9jYWNybC5jcmwwPQYJYIZIAYb4Qg\r\n - EDBDAWLmh0dHA6Ly9jYS5ncmlkLXN1cHBvcnQuYWMudWsvcHViL2NybC9jYWNybC\r\n - 5jcmwwPwYDVR0fBDgwNjA0oDKgMIYuaHR0cDovL2NhLmdyaWQt5hYy51ay9wdWIv\r\n - Y3JsL2NhY3JsLmNybDANBgkqhkiG9w0BAQUFAAOCAQEAS/U4iiooBENGW/Hwmmd3\r\n - XCy6Zrt08YjKCzGNjorT98g8uGsqYjSxv/hmi0qlnlHs+k/3Iobc3LjS5AMYr5L8\r\n - UO7OSkgFFlLHQyC9JzPfmLCAugvzEbyv4Olnsr8hbxF1MbKZoQxUZtMVu29wjfXk\r\n - hTeApBv7eaKCWpSp7MCbvgzm74izKhu3vlDk9w6qVrxePfGgpKPqfHiOoGhFnbTK\r\n - wTC6o2xq5y0qZ03JonF7OJspEd3I5zKY3E+ov7/ZhW6DqT8UFvsAdjvQbXyhV8Eu\r\n - Yhixw1aKEPzNjNowuIseVogKOLXxWI5vAi5HgXdS0/ES5gDGsABo4fqovUKlgop3\r\n - RA==\r\n - -----END CERTIFICATE-----""".replace("\n\n", "\n") +certificate = """-----BEGIN CERTIFICATE----- + MIIFbTCCBFWgAwIBAgICH4cwDQYJKoZIhvcNAQEFBQAwcDELMAkGA1UEBhMCVUsx + ETAPBgNVBAoTCGVTY2llbmNlMRIwEAYDVQQLEwlBdXRob3JpdHkxCzAJBgNVBAMT + AkNBMS0wKwYJKoZIhvcNAQkBFh5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMu + dWswHhcNMDYwNzI3MTQxMzI4WhcNMDcwNzI3MTQxMzI4WjBbMQswCQYDVQQGEwJV + SzERMA8GA1UEChMIZVNjaWVuY2UxEzARBgNVBAsTCk1hbmNoZXN0ZXIxCzAJBgNV + BAcTmrsogriqMWLAk1DMRcwFQYDVQQDEw5taWNoYWVsIHBhcmQYJKoZIhvcNAQEB + BQADggEPADCCAQoCggEBANPEQBgl1IaKdSS1TbhF3hEXSl72G9J+WC/1R64fAcEF + W51rEyFYiIeZGx/BVzwXbeBoNUK41OK65sxGuflMo5gLflbwJtHBRIEKAfVVp3YR + gW7cMA/s/XKgL1GEC7rQw8lIZT8RApukCGqOVHSi/F1SiFlPDxuDfmdiNzL31+sL + 0iwHDdNkGjy5pyBSB8Y79dsSJtCW/iaLB0/n8Sj7HgvvZJ7x0fr+RQjYOUUfrePP + u2MSpFyf+9BbC/aXgaZuiCvSR+8Snv3xApQY+fULK/xY8h8Ua51iXoQ5jrgu2SqR + wgA7BUi3G8LFzMBl8FRCDYGUDy7M6QaHXx1ZWIPWNKsCAwEAAaOCAiQwggIgMAwG + 1UdEwEB/wQCMAAwEQYJYIZIAYb4QgHTTPAQDAgWgMA4GA1UdDwEB/wQEAwID6DAs + BglghkgBhvhCAQ0EHxYdVUsgZS1TY2llbmNlIFVzZXIgQ2VydGlmaWNhdGUwHQYD + VR0OBBYEFDTt/sf9PeMaZDHkUIldrDYMNTBZMIGaBgNVHSMEgZIwgY+AFAI4qxGj + loCLDdMVKwiljjDastqooXSkcjBwMQswCQYDVQQGEwJVSzERMA8GA1UEChMIZVNj + aWVuY2UxEjAQBgNVBAsTCUF1dGhvcml0eTELMAkGA1UEAxMCQ0ExLTArBgkqhkiG + 9w0BCQEWHmNhLW9wZXJhdG9yQGdyaWQtc3VwcG9ydC5hYy51a4IBADApBgNVHRIE + IjAggR5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMudWswGQYDVR0gBBIwEDAO + BgwrBgEEAdkvAQEBAQYwPQYJYIZIAYb4QgEEBDAWLmh0dHA6Ly9jYS5ncmlkLXN1 + cHBvcnQuYWMudmT4sopwqlBWsvcHViL2NybC9jYWNybC5jcmwwPQYJYIZIAYb4Qg + EDBDAWLmh0dHA6Ly9jYS5ncmlkLXN1cHBvcnQuYWMudWsvcHViL2NybC9jYWNybC + 5jcmwwPwYDVR0fBDgwNjA0oDKgMIYuaHR0cDovL2NhLmdyaWQt5hYy51ay9wdWIv + Y3JsL2NhY3JsLmNybDANBgkqhkiG9w0BAQUFAAOCAQEAS/U4iiooBENGW/Hwmmd3 + XCy6Zrt08YjKCzGNjorT98g8uGsqYjSxv/hmi0qlnlHs+k/3Iobc3LjS5AMYr5L8 + UO7OSkgFFlLHQyC9JzPfmLCAugvzEbyv4Olnsr8hbxF1MbKZoQxUZtMVu29wjfXk + hTeApBv7eaKCWpSp7MCbvgzm74izKhu3vlDk9w6qVrxePfGgpKPqfHiOoGhFnbTK + wTC6o2xq5y0qZ03JonF7OJspEd3I5zKY3E+ov7/ZhW6DqT8UFvsAdjvQbXyhV8Eu + Yhixw1aKEPzNjNowuIseVogKOLXxWI5vAi5HgXdS0/ES5gDGsABo4fqovUKlgop3 + RA== + -----END CERTIFICATE-----""".replace("\n", "") request = { "method": "GET", Index: gunicorn-20.1.0/tests/requests/valid/031.http =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/valid/031.http @@ -0,0 +1,2 @@ +-BLARGH /foo HTTP/1.1\r\n +\r\n Index: gunicorn-20.1.0/tests/requests/valid/031.py =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/valid/031.py @@ -0,0 +1,7 @@ +request = { + "method": "-BLARGH", + "uri": uri("/foo"), + "version": (1, 1), + "headers": [], + "body": b"" +} Index: gunicorn-20.1.0/gunicorn/http/errors.py =================================================================== --- gunicorn-20.1.0.orig/gunicorn/http/errors.py +++ gunicorn-20.1.0/gunicorn/http/errors.py @@ -22,6 +22,15 @@ class NoMoreData(IOError): return "No more data after: %r" % self.buf +class ConfigurationProblem(ParseException): + def __init__(self, info): + self.info = info + self.code = 500 + + def __str__(self): + return "Configuration problem: %s" % self.info + + class InvalidRequestLine(ParseException): def __init__(self, req): self.req = req @@ -64,6 +73,15 @@ class InvalidHeaderName(ParseException): return "Invalid HTTP header name: %r" % self.hdr +class UnsupportedTransferCoding(ParseException): + def __init__(self, hdr): + self.hdr = hdr + self.code = 501 + + def __str__(self): + return "Unsupported transfer coding: %r" % self.hdr + + class InvalidChunkSize(IOError): def __init__(self, data): self.data = data Index: gunicorn-20.1.0/SECURITY.md =================================================================== --- /dev/null +++ gunicorn-20.1.0/SECURITY.md @@ -0,0 +1,22 @@ +# Security Policy + +## Reporting a Vulnerability + +**Please note that public Github issues are open for everyone to see!** + +If you believe you are found a problem in Gunicorn software, examples or documentation, we encourage you to send your report privately via email, or via Github using the *Report a vulnerability* button in the [Security](https://github.com/benoitc/gunicorn/security) section. + +## Supported Releases + +At this time, **only the latest release** receives any security attention whatsoever. + +| Version | Status | +| ------- | ------------------ | +| latest release | :white_check_mark: | +| 21.2.0 | :x: | +| 20.0.0 | :x: | +| < 20.0 | :x: | + +## Python Versions + +Gunicorn runs on Python 3.7+, we *highly recommend* the latest release of a [supported series](https://devguide.python.org/version/) and will not prioritize issues exclusively affecting in EoL environments. Index: gunicorn-20.1.0/tests/test_http.py =================================================================== --- gunicorn-20.1.0.orig/tests/test_http.py +++ gunicorn-20.1.0/tests/test_http.py @@ -10,6 +10,17 @@ from gunicorn.http.body import Body, Len from gunicorn.http.wsgi import Response from gunicorn.http.unreader import Unreader, IterUnreader, SocketUnreader from gunicorn.http.errors import InvalidHeader, InvalidHeaderName +from gunicorn.http.message import TOKEN_RE + + +def test_method_pattern(): + assert TOKEN_RE.fullmatch("GET") + assert TOKEN_RE.fullmatch("MKCALENDAR") + assert not TOKEN_RE.fullmatch("GET:") + assert not TOKEN_RE.fullmatch("GET;") + RFC9110_5_6_2_TOKEN_DELIM = r'"(),/:;<=>?@[\]{}' + for bad_char in RFC9110_5_6_2_TOKEN_DELIM: + assert not TOKEN_RE.match(bad_char) def assert_readline(payload, size, expected): Index: gunicorn-20.1.0/gunicorn/config.py =================================================================== --- gunicorn-20.1.0.orig/gunicorn/config.py +++ gunicorn-20.1.0/gunicorn/config.py @@ -2116,5 +2116,131 @@ class StripHeaderSpaces(Setting): This is known to induce vulnerabilities and is not compliant with the HTTP/1.1 standard. See https://portswigger.net/research/http-desync-attacks-request-smuggling-reborn. - Use with care and only if necessary. + Use with care and only if necessary. May be removed in a future version. + + .. versionadded:: 20.0.1 + """ + + +class PermitUnconventionalHTTPMethod(Setting): + name = "permit_unconventional_http_method" + section = "Server Mechanics" + cli = ["--permit-unconventional-http-method"] + validator = validate_bool + action = "store_true" + default = False + desc = """\ + Permit HTTP methods not matching conventions, such as IANA registration guidelines + + This permits request methods of length less than 3 or more than 20, + methods with lowercase characters or methods containing the # character. + HTTP methods are case sensitive by definition, and merely uppercase by convention. + + This option is provided to diagnose backwards-incompatible changes. + + Use with care and only if necessary. May be removed in a future version. + + .. versionadded:: 22.0.0 + """ + + +class PermitUnconventionalHTTPVersion(Setting): + name = "permit_unconventional_http_version" + section = "Server Mechanics" + cli = ["--permit-unconventional-http-version"] + validator = validate_bool + action = "store_true" + default = False + desc = """\ + Permit HTTP version not matching conventions of 2023 + + This disables the refusal of likely malformed request lines. + It is unusual to specify HTTP 1 versions other than 1.0 and 1.1. + + This option is provided to diagnose backwards-incompatible changes. + Use with care and only if necessary. May be removed in a future version. + + .. versionadded:: 22.0.0 + """ + + +class CasefoldHTTPMethod(Setting): + name = "casefold_http_method" + section = "Server Mechanics" + cli = ["--casefold-http-method"] + validator = validate_bool + action = "store_true" + default = False + desc = """\ + Transform received HTTP methods to uppercase + + HTTP methods are case sensitive by definition, and merely uppercase by convention. + + This option is provided because previous versions of gunicorn defaulted to this behaviour. + + Use with care and only if necessary. May be removed in a future version. + + .. versionadded:: 22.0.0 + """ + + +def validate_header_map_behaviour(val): + # FIXME: refactor all of this subclassing stdlib argparse + + if val is None: + return + + if not isinstance(val, str): + raise TypeError("Invalid type for casting: %s" % val) + if val.lower().strip() == "drop": + return "drop" + elif val.lower().strip() == "refuse": + return "refuse" + elif val.lower().strip() == "dangerous": + return "dangerous" + else: + raise ValueError("Invalid header map behaviour: %s" % val) + + +class HeaderMap(Setting): + name = "header_map" + section = "Server Mechanics" + cli = ["--header-map"] + validator = validate_header_map_behaviour + default = "drop" + desc = """\ + Configure how header field names are mapped into environ + + Headers containing underscores are permitted by RFC9110, + but gunicorn joining headers of different names into + the same environment variable will dangerously confuse applications as to which is which. + + The safe default ``drop`` is to silently drop headers that cannot be unambiguously mapped. + The value ``refuse`` will return an error if a request contains *any* such header. + The value ``dangerous`` matches the previous, not advisabble, behaviour of mapping different + header field names into the same environ name. + + Use with care and only if necessary and after considering if your problem could + instead be solved by specifically renaming or rewriting only the intended headers + on a proxy in front of Gunicorn. + + .. versionadded:: 22.0.0 + """ + + +class TolerateDangerousFraming(Setting): + name = "tolerate_dangerous_framing" + section = "Server Mechanics" + cli = ["--tolerate-dangerous-framing"] + validator = validate_bool + action = "store_true" + default = False + desc = """\ + Process requests with both Transfer-Encoding and Content-Length + + This is known to induce vulnerabilities, but not strictly forbidden by RFC9112. + + Use with care and only if necessary. May be removed in a future version. + + .. versionadded:: 22.0.0 """ Index: gunicorn-20.1.0/tests/requests/invalid/003b.http =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/003b.http @@ -0,0 +1,2 @@ +bla:rgh /foo HTTP/1.1\r\n +\r\n Index: gunicorn-20.1.0/tests/requests/invalid/003b.py =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/003b.py @@ -0,0 +1,2 @@ +from gunicorn.http.errors import InvalidRequestMethod +request = InvalidRequestMethod \ No newline at end of file Index: gunicorn-20.1.0/tests/requests/invalid/003c.http =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/003c.http @@ -0,0 +1,2 @@ +-bl /foo HTTP/1.1\r\n +\r\n Index: gunicorn-20.1.0/tests/requests/invalid/003c.py =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/003c.py @@ -0,0 +1,2 @@ +from gunicorn.http.errors import InvalidRequestMethod +request = InvalidRequestMethod Index: gunicorn-20.1.0/tests/requests/valid/031compat.http =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/valid/031compat.http @@ -0,0 +1,2 @@ +-blargh /foo HTTP/1.1\r\n +\r\n \ No newline at end of file Index: gunicorn-20.1.0/tests/requests/valid/031compat.py =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/valid/031compat.py @@ -0,0 +1,13 @@ +from gunicorn.config import Config + +cfg = Config() +cfg.set("permit_unconventional_http_method", True) +cfg.set("casefold_http_method", True) + +request = { + "method": "-BLARGH", + "uri": uri("/foo"), + "version": (1, 1), + "headers": [], + "body": b"" +} Index: gunicorn-20.1.0/tests/requests/valid/031compat2.http =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/valid/031compat2.http @@ -0,0 +1,2 @@ +-blargh /foo HTTP/1.1\r\n +\r\n Index: gunicorn-20.1.0/tests/requests/valid/031compat2.py =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/valid/031compat2.py @@ -0,0 +1,12 @@ +from gunicorn.config import Config + +cfg = Config() +cfg.set("permit_unconventional_http_method", True) + +request = { + "method": "-blargh", + "uri": uri("/foo"), + "version": (1, 1), + "headers": [], + "body": b"" +} Index: gunicorn-20.1.0/tests/requests/invalid/040.http =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/040.http @@ -0,0 +1,6 @@ +GET /keep/same/as?invalid/040 HTTP/1.0\r\n +Transfer_Encoding: tricked\r\n +Content-Length: 7\r\n +Content_Length: -1E23\r\n +\r\n +tricked\r\n Index: gunicorn-20.1.0/tests/requests/invalid/040.py =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/040.py @@ -0,0 +1,7 @@ +from gunicorn.http.errors import InvalidHeaderName +from gunicorn.config import Config + +cfg = Config() +cfg.set("header_map", "refuse") + +request = InvalidHeaderName Index: gunicorn-20.1.0/tests/requests/invalid/chunked_07.http =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/chunked_07.http @@ -0,0 +1,10 @@ +POST /chunked_ambiguous_header_mapping HTTP/1.1\r\n +Transfer_Encoding: gzip\r\n +Transfer-Encoding: chunked\r\n +\r\n +5\r\n +hello\r\n +6\r\n + world\r\n +0\r\n +\r\n Index: gunicorn-20.1.0/tests/requests/invalid/chunked_07.py =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/chunked_07.py @@ -0,0 +1,7 @@ +from gunicorn.http.errors import InvalidHeaderName +from gunicorn.config import Config + +cfg = Config() +cfg.set("header_map", "refuse") + +request = InvalidHeaderName Index: gunicorn-20.1.0/tests/requests/valid/040.http =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/valid/040.http @@ -0,0 +1,6 @@ +GET /keep/same/as?invalid/040 HTTP/1.0\r\n +Transfer_Encoding: tricked\r\n +Content-Length: 7\r\n +Content_Length: -1E23\r\n +\r\n +tricked\r\n Index: gunicorn-20.1.0/tests/requests/valid/040.py =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/valid/040.py @@ -0,0 +1,9 @@ +request = { + "method": "GET", + "uri": uri("/keep/same/as?invalid/040"), + "version": (1, 0), + "headers": [ + ("CONTENT-LENGTH", "7") + ], + "body": b'tricked' +} Index: gunicorn-20.1.0/tests/requests/valid/040_compat.http =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/valid/040_compat.http @@ -0,0 +1,6 @@ +GET /keep/same/as?invalid/040 HTTP/1.0\r\n +Transfer_Encoding: tricked\r\n +Content-Length: 7\r\n +Content_Length: -1E23\r\n +\r\n +tricked\r\n Index: gunicorn-20.1.0/tests/requests/valid/040_compat.py =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/valid/040_compat.py @@ -0,0 +1,16 @@ +from gunicorn.config import Config + +cfg = Config() +cfg.set("header_map", "dangerous") + +request = { + "method": "GET", + "uri": uri("/keep/same/as?invalid/040"), + "version": (1, 0), + "headers": [ + ("TRANSFER_ENCODING", "tricked"), + ("CONTENT-LENGTH", "7"), + ("CONTENT_LENGTH", "-1E23"), + ], + "body": b'tricked' +} Index: gunicorn-20.1.0/gunicorn/workers/base.py =================================================================== --- gunicorn-20.1.0.orig/gunicorn/workers/base.py +++ gunicorn-20.1.0/gunicorn/workers/base.py @@ -249,6 +249,8 @@ class Worker(object): else: if hasattr(req, "uri"): self.log.exception("Error handling request %s", req.uri) + else: + self.log.exception("Error handling request (no URI read)") status_int = 500 reason = "Internal Server Error" mesg = "" Index: gunicorn-20.1.0/tests/requests/invalid/chunked_01.http =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/chunked_01.http @@ -0,0 +1,12 @@ +POST /chunked_w_underscore_chunk_size HTTP/1.1\r\n +Transfer-Encoding: chunked\r\n +\r\n +5\r\n +hello\r\n +6_0\r\n + world\r\n +0\r\n +\r\n +POST /after HTTP/1.1\r\n +Transfer-Encoding: identity\r\n +\r\n Index: gunicorn-20.1.0/tests/requests/invalid/chunked_01.py =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/chunked_01.py @@ -0,0 +1,2 @@ +from gunicorn.http.errors import InvalidChunkSize +request = InvalidChunkSize Index: gunicorn-20.1.0/tests/requests/invalid/chunked_02.http =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/chunked_02.http @@ -0,0 +1,9 @@ +POST /chunked_with_prefixed_value HTTP/1.1\r\n +Content-Length: 12\r\n +Transfer-Encoding: \tchunked\r\n +\r\n +5\r\n +hello\r\n +6\r\n + world\r\n +\r\n Index: gunicorn-20.1.0/tests/requests/invalid/chunked_02.py =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/chunked_02.py @@ -0,0 +1,2 @@ +from gunicorn.http.errors import InvalidHeader +request = InvalidHeader Index: gunicorn-20.1.0/tests/requests/invalid/chunked_03.http =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/chunked_03.http @@ -0,0 +1,8 @@ +POST /double_chunked HTTP/1.1\r\n +Transfer-Encoding: identity, chunked, identity, chunked\r\n +\r\n +5\r\n +hello\r\n +6\r\n + world\r\n +\r\n Index: gunicorn-20.1.0/tests/requests/invalid/chunked_03.py =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/chunked_03.py @@ -0,0 +1,2 @@ +from gunicorn.http.errors import UnsupportedTransferCoding +request = UnsupportedTransferCoding Index: gunicorn-20.1.0/tests/requests/invalid/chunked_04.http =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/chunked_04.http @@ -0,0 +1,11 @@ +POST /chunked_twice HTTP/1.1\r\n +Transfer-Encoding: identity\r\n +Transfer-Encoding: chunked\r\n +Transfer-Encoding: identity\r\n +Transfer-Encoding: chunked\r\n +\r\n +5\r\n +hello\r\n +6\r\n + world\r\n +\r\n Index: gunicorn-20.1.0/tests/requests/invalid/chunked_04.py =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/chunked_04.py @@ -0,0 +1,2 @@ +from gunicorn.http.errors import InvalidHeader +request = InvalidHeader Index: gunicorn-20.1.0/tests/requests/invalid/chunked_05.http =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/chunked_05.http @@ -0,0 +1,11 @@ +POST /chunked_HTTP_1.0 HTTP/1.0\r\n +Transfer-Encoding: chunked\r\n +\r\n +5\r\n +hello\r\n +6\r\n + world\r\n +0\r\n +Vary: *\r\n +Content-Type: text/plain\r\n +\r\n Index: gunicorn-20.1.0/tests/requests/invalid/chunked_05.py =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/chunked_05.py @@ -0,0 +1,2 @@ +from gunicorn.http.errors import InvalidHeader +request = InvalidHeader Index: gunicorn-20.1.0/tests/requests/invalid/chunked_06.http =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/chunked_06.http @@ -0,0 +1,9 @@ +POST /chunked_not_last HTTP/1.1\r\n +Transfer-Encoding: chunked\r\n +Transfer-Encoding: gzip\r\n +\r\n +5\r\n +hello\r\n +6\r\n + world\r\n +\r\n Index: gunicorn-20.1.0/tests/requests/invalid/chunked_06.py =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/chunked_06.py @@ -0,0 +1,2 @@ +from gunicorn.http.errors import UnsupportedTransferCoding +request = UnsupportedTransferCoding Index: gunicorn-20.1.0/tests/requests/invalid/chunked_08.http =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/chunked_08.http @@ -0,0 +1,9 @@ +POST /chunked_not_last HTTP/1.1\r\n +Transfer-Encoding: chunked\r\n +Transfer-Encoding: identity\r\n +\r\n +5\r\n +hello\r\n +6\r\n + world\r\n +\r\n Index: gunicorn-20.1.0/tests/requests/invalid/chunked_08.py =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/chunked_08.py @@ -0,0 +1,2 @@ +from gunicorn.http.errors import InvalidHeader +request = InvalidHeader Index: gunicorn-20.1.0/tests/requests/invalid/nonascii_01.http =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/nonascii_01.http @@ -0,0 +1,4 @@ +GETß /germans.. HTTP/1.1\r\n +Content-Length: 3\r\n +\r\n +ÄÄÄ Index: gunicorn-20.1.0/tests/requests/invalid/nonascii_01.py =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/nonascii_01.py @@ -0,0 +1,5 @@ +from gunicorn.config import Config +from gunicorn.http.errors import InvalidRequestMethod + +cfg = Config() +request = InvalidRequestMethod Index: gunicorn-20.1.0/tests/requests/invalid/nonascii_02.http =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/nonascii_02.http @@ -0,0 +1,4 @@ +GETÿ /french.. HTTP/1.1\r\n +Content-Length: 3\r\n +\r\n +ÄÄÄ Index: gunicorn-20.1.0/tests/requests/invalid/nonascii_02.py =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/nonascii_02.py @@ -0,0 +1,5 @@ +from gunicorn.config import Config +from gunicorn.http.errors import InvalidRequestMethod + +cfg = Config() +request = InvalidRequestMethod Index: gunicorn-20.1.0/tests/requests/invalid/nonascii_04.http =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/nonascii_04.http @@ -0,0 +1,5 @@ +GET /french.. HTTP/1.1\r\n +Content-Lengthÿ: 3\r\n +Content-Length: 3\r\n +\r\n +ÄÄÄ Index: gunicorn-20.1.0/tests/requests/invalid/nonascii_04.py =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/nonascii_04.py @@ -0,0 +1,5 @@ +from gunicorn.config import Config +from gunicorn.http.errors import InvalidHeaderName + +cfg = Config() +request = InvalidHeaderName Index: gunicorn-20.1.0/tests/requests/invalid/prefix_01.http =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/prefix_01.http @@ -0,0 +1,2 @@ +GET\0PROXY /foo HTTP/1.1\r\n +\r\n Index: gunicorn-20.1.0/tests/requests/invalid/prefix_01.py =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/prefix_01.py @@ -0,0 +1,2 @@ +from gunicorn.http.errors import InvalidRequestMethod +request = InvalidRequestMethod \ No newline at end of file Index: gunicorn-20.1.0/tests/requests/invalid/prefix_02.http =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/prefix_02.http @@ -0,0 +1,2 @@ +GET\0 /foo HTTP/1.1\r\n +\r\n Index: gunicorn-20.1.0/tests/requests/invalid/prefix_02.py =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/prefix_02.py @@ -0,0 +1,2 @@ +from gunicorn.http.errors import InvalidRequestMethod +request = InvalidRequestMethod \ No newline at end of file Index: gunicorn-20.1.0/tests/requests/invalid/prefix_03.http =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/prefix_03.http @@ -0,0 +1,4 @@ +GET /stuff/here?foo=bar HTTP/1.1\r\n +Content-Length: 0 1\r\n +\r\n +x Index: gunicorn-20.1.0/tests/requests/invalid/prefix_03.py =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/prefix_03.py @@ -0,0 +1,5 @@ +from gunicorn.config import Config +from gunicorn.http.errors import InvalidHeader + +cfg = Config() +request = InvalidHeader Index: gunicorn-20.1.0/tests/requests/invalid/prefix_04.http =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/prefix_04.http @@ -0,0 +1,5 @@ +GET /stuff/here?foo=bar HTTP/1.1\r\n +Content-Length: 3 1\r\n +\r\n +xyz +abc123 Index: gunicorn-20.1.0/tests/requests/invalid/prefix_04.py =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/prefix_04.py @@ -0,0 +1,5 @@ +from gunicorn.config import Config +from gunicorn.http.errors import InvalidHeader + +cfg = Config() +request = InvalidHeader Index: gunicorn-20.1.0/tests/requests/invalid/prefix_05.http =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/prefix_05.http @@ -0,0 +1,4 @@ +GET: /stuff/here?foo=bar HTTP/1.1\r\n +Content-Length: 3\r\n +\r\n +xyz Index: gunicorn-20.1.0/tests/requests/invalid/prefix_05.py =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/prefix_05.py @@ -0,0 +1,5 @@ +from gunicorn.config import Config +from gunicorn.http.errors import InvalidRequestMethod + +cfg = Config() +request = InvalidRequestMethod Index: gunicorn-20.1.0/tests/requests/valid/025.http =================================================================== --- gunicorn-20.1.0.orig/tests/requests/valid/025.http +++ gunicorn-20.1.0/tests/requests/valid/025.http @@ -1,10 +1,9 @@ POST /chunked_cont_h_at_first HTTP/1.1\r\n -Content-Length: -1\r\n Transfer-Encoding: chunked\r\n \r\n 5; some; parameters=stuff\r\n hello\r\n -6; blahblah; blah\r\n +6 \t;\tblahblah; blah\r\n world\r\n 0\r\n \r\n @@ -16,4 +15,10 @@ Content-Length: -1\r\n hello\r\n 6; blahblah; blah\r\n world\r\n -0\r\n \ No newline at end of file +0\r\n +\r\n +PUT /ignored_after_dangerous_framing HTTP/1.1\r\n +Content-Length: 3\r\n +\r\n +foo\r\n +\r\n Index: gunicorn-20.1.0/tests/requests/valid/025.py =================================================================== --- gunicorn-20.1.0.orig/tests/requests/valid/025.py +++ gunicorn-20.1.0/tests/requests/valid/025.py @@ -1,9 +1,13 @@ +from gunicorn.config import Config + +cfg = Config() +cfg.set("tolerate_dangerous_framing", True) + req1 = { "method": "POST", "uri": uri("/chunked_cont_h_at_first"), "version": (1, 1), "headers": [ - ("CONTENT-LENGTH", "-1"), ("TRANSFER-ENCODING", "chunked") ], "body": b"hello world" Index: gunicorn-20.1.0/tests/requests/valid/025compat.http =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/valid/025compat.http @@ -0,0 +1,18 @@ +POST /chunked_cont_h_at_first HTTP/1.1\r\n +Transfer-Encoding: chunked\r\n +\r\n +5; some; parameters=stuff\r\n +hello\r\n +6; blahblah; blah\r\n + world\r\n +0\r\n +\r\n +PUT /chunked_cont_h_at_last HTTP/1.1\r\n +Transfer-Encoding: chunked\r\n +Content-Length: -1\r\n +\r\n +5; some; parameters=stuff\r\n +hello\r\n +6; blahblah; blah\r\n + world\r\n +0\r\n Index: gunicorn-20.1.0/tests/requests/valid/025compat.py =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/valid/025compat.py @@ -0,0 +1,27 @@ +from gunicorn.config import Config + +cfg = Config() +cfg.set("tolerate_dangerous_framing", True) + +req1 = { + "method": "POST", + "uri": uri("/chunked_cont_h_at_first"), + "version": (1, 1), + "headers": [ + ("TRANSFER-ENCODING", "chunked") + ], + "body": b"hello world" +} + +req2 = { + "method": "PUT", + "uri": uri("/chunked_cont_h_at_last"), + "version": (1, 1), + "headers": [ + ("TRANSFER-ENCODING", "chunked"), + ("CONTENT-LENGTH", "-1"), + ], + "body": b"hello world" +} + +request = [req1, req2] Index: gunicorn-20.1.0/tests/requests/valid/029.http =================================================================== --- gunicorn-20.1.0.orig/tests/requests/valid/029.http +++ gunicorn-20.1.0/tests/requests/valid/029.http @@ -1,6 +1,6 @@ GET /stuff/here?foo=bar HTTP/1.1\r\n -Transfer-Encoding: chunked\r\n Transfer-Encoding: identity\r\n +Transfer-Encoding: chunked\r\n \r\n 5\r\n hello\r\n Index: gunicorn-20.1.0/tests/requests/valid/029.py =================================================================== --- gunicorn-20.1.0.orig/tests/requests/valid/029.py +++ gunicorn-20.1.0/tests/requests/valid/029.py @@ -7,8 +7,8 @@ request = { "uri": uri("/stuff/here?foo=bar"), "version": (1, 1), "headers": [ + ('TRANSFER-ENCODING', 'identity'), ('TRANSFER-ENCODING', 'chunked'), - ('TRANSFER-ENCODING', 'identity') ], "body": b"hello" } Index: gunicorn-20.1.0/tests/requests/invalid/nonascii_03.http =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/nonascii_03.http @@ -0,0 +1,5 @@ +GET /germans.. HTTP/1.1\r\n +Content-Lengthß: 3\r\n +Content-Length: 3\r\n +\r\n +ÄÄÄ Index: gunicorn-20.1.0/tests/requests/invalid/nonascii_03.py =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/nonascii_03.py @@ -0,0 +1,5 @@ +from gunicorn.config import Config +from gunicorn.http.errors import InvalidHeaderName + +cfg = Config() +request = InvalidHeaderName Index: gunicorn-20.1.0/tests/requests/invalid/prefix_06.http =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/prefix_06.http @@ -0,0 +1,4 @@ +GET /the/future HTTP/1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111.1\r\n +Content-Length: 7\r\n +\r\n +Old Man Index: gunicorn-20.1.0/tests/requests/invalid/prefix_06.py =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/prefix_06.py @@ -0,0 +1,5 @@ +from gunicorn.config import Config +from gunicorn.http.errors import InvalidHTTPVersion + +cfg = Config() +request = InvalidHTTPVersion Index: gunicorn-20.1.0/tests/requests/invalid/version_01.http =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/version_01.http @@ -0,0 +1,2 @@ +GET /foo HTTP/0.99\r\n +\r\n Index: gunicorn-20.1.0/tests/requests/invalid/version_01.py =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/version_01.py @@ -0,0 +1,2 @@ +from gunicorn.http.errors import InvalidHTTPVersion +request = InvalidHTTPVersion Index: gunicorn-20.1.0/tests/requests/invalid/version_02.http =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/version_02.http @@ -0,0 +1,2 @@ +GET /foo HTTP/2.0\r\n +\r\n Index: gunicorn-20.1.0/tests/requests/invalid/version_02.py =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/version_02.py @@ -0,0 +1,2 @@ +from gunicorn.http.errors import InvalidHTTPVersion +request = InvalidHTTPVersion Index: gunicorn-20.1.0/tests/requests/invalid/chunked_09.http =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/chunked_09.http @@ -0,0 +1,7 @@ +POST /chunked_ows_without_ext HTTP/1.1\r\n +Transfer-Encoding: chunked\r\n +\r\n +5\r\n +hello\r\n +0 \r\n +\r\n Index: gunicorn-20.1.0/tests/requests/invalid/chunked_09.py =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/chunked_09.py @@ -0,0 +1,2 @@ +from gunicorn.http.errors import InvalidChunkSize +request = InvalidChunkSize Index: gunicorn-20.1.0/tests/requests/invalid/chunked_10.http =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/chunked_10.http @@ -0,0 +1,7 @@ +POST /chunked_ows_before HTTP/1.1\r\n +Transfer-Encoding: chunked\r\n +\r\n +5\r\n +hello\r\n + 0\r\n +\r\n Index: gunicorn-20.1.0/tests/requests/invalid/chunked_10.py =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/chunked_10.py @@ -0,0 +1,2 @@ +from gunicorn.http.errors import InvalidChunkSize +request = InvalidChunkSize Index: gunicorn-20.1.0/tests/requests/invalid/chunked_11.http =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/chunked_11.http @@ -0,0 +1,7 @@ +POST /chunked_ows_before HTTP/1.1\r\n +Transfer-Encoding: chunked\r\n +\r\n +5\n;\r\n +hello\r\n +0\r\n +\r\n Index: gunicorn-20.1.0/tests/requests/invalid/chunked_11.py =================================================================== --- /dev/null +++ gunicorn-20.1.0/tests/requests/invalid/chunked_11.py @@ -0,0 +1,2 @@ +from gunicorn.http.errors import InvalidChunkSize +request = InvalidChunkSize
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor