Version in base suite: 20.1.0-6 Base version: gunicorn_20.1.0-6 Target version: gunicorn_20.1.0-6+deb12u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/g/gunicorn/gunicorn_20.1.0-6.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/g/gunicorn/gunicorn_20.1.0-6+deb12u1.dsc changelog | 7 patches/0001-fail-safe-on-unsupported-request-framing.patch | 692 ++++++++++ patches/0002-RFC-compliant-header-field-chunk-validation.patch | 59 patches/0003-Disallow-empty-header-names.patch | 25 patches/0004-RFC-compliant-request-line-and-header-parsing.patch | 316 ++++ patches/0005-pytest-raise-on-malformed-test-fixtures.patch | 57 patches/series | 5 7 files changed, 1161 insertions(+) diff -Nru gunicorn-20.1.0/debian/changelog gunicorn-20.1.0/debian/changelog --- gunicorn-20.1.0/debian/changelog 2022-10-31 17:36:51.000000000 +0000 +++ gunicorn-20.1.0/debian/changelog 2024-12-20 03:42:55.000000000 +0000 @@ -1,3 +1,10 @@ +gunicorn (20.1.0-6+deb12u1) bookworm; urgency=medium + + * Non-maintainer upload. + * CVE-2024-1135: HTTP Request Smuggling (Closes: #1069126) + + -- Adrian Bunk Fri, 20 Dec 2024 05:42:55 +0200 + gunicorn (20.1.0-6) unstable; urgency=medium [ Debian Janitor ] diff -Nru gunicorn-20.1.0/debian/patches/0001-fail-safe-on-unsupported-request-framing.patch gunicorn-20.1.0/debian/patches/0001-fail-safe-on-unsupported-request-framing.patch --- gunicorn-20.1.0/debian/patches/0001-fail-safe-on-unsupported-request-framing.patch 1970-01-01 00:00:00.000000000 +0000 +++ gunicorn-20.1.0/debian/patches/0001-fail-safe-on-unsupported-request-framing.patch 2024-12-20 03:39:18.000000000 +0000 @@ -0,0 +1,692 @@ +From f6f3c50e9cd5e0bffe3bec65d7f8690baab9c824 Mon Sep 17 00:00:00 2001 +From: "Paul J. Dorn" +Date: Thu, 7 Dec 2023 09:22:30 +0100 +Subject: fail-safe on unsupported request framing + +If we promise wsgi.input_terminated, we better get it right - or not at all. +* chunked encoding on HTTP <= 1.1 +* chunked not last transfer coding +* multiple chinked codings +* any unknown codings (yes, this too! because we do not detect unusual syntax that is still chunked) +* empty coding (plausibly harmless, but not see in real life anyway - refused, for the moment) +--- + gunicorn/config.py | 18 ++++++++++ + gunicorn/http/errors.py | 9 +++++ + gunicorn/http/message.py | 45 +++++++++++++++++++++++++ + tests/requests/invalid/chunked_01.http | 12 +++++++ + tests/requests/invalid/chunked_01.py | 2 ++ + tests/requests/invalid/chunked_02.http | 9 +++++ + tests/requests/invalid/chunked_02.py | 2 ++ + tests/requests/invalid/chunked_03.http | 8 +++++ + tests/requests/invalid/chunked_03.py | 2 ++ + tests/requests/invalid/chunked_04.http | 11 ++++++ + tests/requests/invalid/chunked_04.py | 2 ++ + tests/requests/invalid/chunked_05.http | 11 ++++++ + tests/requests/invalid/chunked_05.py | 2 ++ + tests/requests/invalid/chunked_06.http | 9 +++++ + tests/requests/invalid/chunked_06.py | 2 ++ + tests/requests/invalid/chunked_08.http | 9 +++++ + tests/requests/invalid/chunked_08.py | 2 ++ + tests/requests/invalid/nonascii_01.http | 4 +++ + tests/requests/invalid/nonascii_01.py | 5 +++ + tests/requests/invalid/nonascii_02.http | 4 +++ + tests/requests/invalid/nonascii_02.py | 5 +++ + tests/requests/invalid/nonascii_04.http | 5 +++ + tests/requests/invalid/nonascii_04.py | 5 +++ + tests/requests/invalid/prefix_01.http | 2 ++ + tests/requests/invalid/prefix_01.py | 2 ++ + tests/requests/invalid/prefix_02.http | 2 ++ + tests/requests/invalid/prefix_02.py | 2 ++ + tests/requests/invalid/prefix_03.http | 4 +++ + tests/requests/invalid/prefix_03.py | 5 +++ + tests/requests/invalid/prefix_04.http | 5 +++ + tests/requests/invalid/prefix_04.py | 5 +++ + tests/requests/invalid/prefix_05.http | 4 +++ + tests/requests/invalid/prefix_05.py | 5 +++ + tests/requests/valid/025.http | 9 +++-- + tests/requests/valid/025.py | 6 +++- + tests/requests/valid/025compat.http | 18 ++++++++++ + tests/requests/valid/025compat.py | 27 +++++++++++++++ + tests/requests/valid/029.http | 2 +- + tests/requests/valid/029.py | 2 +- + tests/treq.py | 4 ++- + 40 files changed, 281 insertions(+), 6 deletions(-) + create mode 100644 tests/requests/invalid/chunked_01.http + create mode 100644 tests/requests/invalid/chunked_01.py + create mode 100644 tests/requests/invalid/chunked_02.http + create mode 100644 tests/requests/invalid/chunked_02.py + create mode 100644 tests/requests/invalid/chunked_03.http + create mode 100644 tests/requests/invalid/chunked_03.py + create mode 100644 tests/requests/invalid/chunked_04.http + create mode 100644 tests/requests/invalid/chunked_04.py + create mode 100644 tests/requests/invalid/chunked_05.http + create mode 100644 tests/requests/invalid/chunked_05.py + create mode 100644 tests/requests/invalid/chunked_06.http + create mode 100644 tests/requests/invalid/chunked_06.py + create mode 100644 tests/requests/invalid/chunked_08.http + create mode 100644 tests/requests/invalid/chunked_08.py + create mode 100644 tests/requests/invalid/nonascii_01.http + create mode 100644 tests/requests/invalid/nonascii_01.py + create mode 100644 tests/requests/invalid/nonascii_02.http + create mode 100644 tests/requests/invalid/nonascii_02.py + create mode 100644 tests/requests/invalid/nonascii_04.http + create mode 100644 tests/requests/invalid/nonascii_04.py + create mode 100644 tests/requests/invalid/prefix_01.http + create mode 100644 tests/requests/invalid/prefix_01.py + create mode 100644 tests/requests/invalid/prefix_02.http + create mode 100644 tests/requests/invalid/prefix_02.py + create mode 100644 tests/requests/invalid/prefix_03.http + create mode 100644 tests/requests/invalid/prefix_03.py + create mode 100644 tests/requests/invalid/prefix_04.http + create mode 100644 tests/requests/invalid/prefix_04.py + create mode 100644 tests/requests/invalid/prefix_05.http + create mode 100644 tests/requests/invalid/prefix_05.py + create mode 100644 tests/requests/valid/025compat.http + create mode 100644 tests/requests/valid/025compat.py + +diff --git a/gunicorn/config.py b/gunicorn/config.py +index 8fd281be..450494cf 100644 +--- a/gunicorn/config.py ++++ b/gunicorn/config.py +@@ -2118,3 +2118,21 @@ class StripHeaderSpaces(Setting): + + Use with care and only if necessary. + """ ++ ++ ++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 ++ """ +diff --git a/gunicorn/http/errors.py b/gunicorn/http/errors.py +index 7839ef05..1ee673b4 100644 +--- a/gunicorn/http/errors.py ++++ b/gunicorn/http/errors.py +@@ -64,6 +64,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 +diff --git a/gunicorn/http/message.py b/gunicorn/http/message.py +index 17d22402..5018a188 100644 +--- a/gunicorn/http/message.py ++++ b/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 +@@ -36,6 +37,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,6 +57,9 @@ 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() + +@@ -132,9 +137,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,6 +193,8 @@ 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() +diff --git a/tests/requests/invalid/chunked_01.http b/tests/requests/invalid/chunked_01.http +new file mode 100644 +index 00000000..7a8e55d2 +--- /dev/null ++++ b/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 +diff --git a/tests/requests/invalid/chunked_01.py b/tests/requests/invalid/chunked_01.py +new file mode 100644 +index 00000000..0571e118 +--- /dev/null ++++ b/tests/requests/invalid/chunked_01.py +@@ -0,0 +1,2 @@ ++from gunicorn.http.errors import InvalidChunkSize ++request = InvalidChunkSize +diff --git a/tests/requests/invalid/chunked_02.http b/tests/requests/invalid/chunked_02.http +new file mode 100644 +index 00000000..9ae49e52 +--- /dev/null ++++ b/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 +diff --git a/tests/requests/invalid/chunked_02.py b/tests/requests/invalid/chunked_02.py +new file mode 100644 +index 00000000..1541eb70 +--- /dev/null ++++ b/tests/requests/invalid/chunked_02.py +@@ -0,0 +1,2 @@ ++from gunicorn.http.errors import InvalidHeader ++request = InvalidHeader +diff --git a/tests/requests/invalid/chunked_03.http b/tests/requests/invalid/chunked_03.http +new file mode 100644 +index 00000000..0bbbfe6e +--- /dev/null ++++ b/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 +diff --git a/tests/requests/invalid/chunked_03.py b/tests/requests/invalid/chunked_03.py +new file mode 100644 +index 00000000..58a34600 +--- /dev/null ++++ b/tests/requests/invalid/chunked_03.py +@@ -0,0 +1,2 @@ ++from gunicorn.http.errors import UnsupportedTransferCoding ++request = UnsupportedTransferCoding +diff --git a/tests/requests/invalid/chunked_04.http b/tests/requests/invalid/chunked_04.http +new file mode 100644 +index 00000000..d47109e3 +--- /dev/null ++++ b/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 +diff --git a/tests/requests/invalid/chunked_04.py b/tests/requests/invalid/chunked_04.py +new file mode 100644 +index 00000000..1541eb70 +--- /dev/null ++++ b/tests/requests/invalid/chunked_04.py +@@ -0,0 +1,2 @@ ++from gunicorn.http.errors import InvalidHeader ++request = InvalidHeader +diff --git a/tests/requests/invalid/chunked_05.http b/tests/requests/invalid/chunked_05.http +new file mode 100644 +index 00000000..014e85ac +--- /dev/null ++++ b/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 +diff --git a/tests/requests/invalid/chunked_05.py b/tests/requests/invalid/chunked_05.py +new file mode 100644 +index 00000000..1541eb70 +--- /dev/null ++++ b/tests/requests/invalid/chunked_05.py +@@ -0,0 +1,2 @@ ++from gunicorn.http.errors import InvalidHeader ++request = InvalidHeader +diff --git a/tests/requests/invalid/chunked_06.http b/tests/requests/invalid/chunked_06.http +new file mode 100644 +index 00000000..ef70faab +--- /dev/null ++++ b/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 +diff --git a/tests/requests/invalid/chunked_06.py b/tests/requests/invalid/chunked_06.py +new file mode 100644 +index 00000000..58a34600 +--- /dev/null ++++ b/tests/requests/invalid/chunked_06.py +@@ -0,0 +1,2 @@ ++from gunicorn.http.errors import UnsupportedTransferCoding ++request = UnsupportedTransferCoding +diff --git a/tests/requests/invalid/chunked_08.http b/tests/requests/invalid/chunked_08.http +new file mode 100644 +index 00000000..8d4aaa6e +--- /dev/null ++++ b/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 +diff --git a/tests/requests/invalid/chunked_08.py b/tests/requests/invalid/chunked_08.py +new file mode 100644 +index 00000000..1541eb70 +--- /dev/null ++++ b/tests/requests/invalid/chunked_08.py +@@ -0,0 +1,2 @@ ++from gunicorn.http.errors import InvalidHeader ++request = InvalidHeader +diff --git a/tests/requests/invalid/nonascii_01.http b/tests/requests/invalid/nonascii_01.http +new file mode 100644 +index 00000000..30d18cd6 +--- /dev/null ++++ b/tests/requests/invalid/nonascii_01.http +@@ -0,0 +1,4 @@ ++GETß /germans.. HTTP/1.1\r\n ++Content-Length: 3\r\n ++\r\n ++ÄÄÄ +diff --git a/tests/requests/invalid/nonascii_01.py b/tests/requests/invalid/nonascii_01.py +new file mode 100644 +index 00000000..0da10f42 +--- /dev/null ++++ b/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 +diff --git a/tests/requests/invalid/nonascii_02.http b/tests/requests/invalid/nonascii_02.http +new file mode 100644 +index 00000000..36a61703 +--- /dev/null ++++ b/tests/requests/invalid/nonascii_02.http +@@ -0,0 +1,4 @@ ++GETÿ /french.. HTTP/1.1\r\n ++Content-Length: 3\r\n ++\r\n ++ÄÄÄ +diff --git a/tests/requests/invalid/nonascii_02.py b/tests/requests/invalid/nonascii_02.py +new file mode 100644 +index 00000000..0da10f42 +--- /dev/null ++++ b/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 +diff --git a/tests/requests/invalid/nonascii_04.http b/tests/requests/invalid/nonascii_04.http +new file mode 100644 +index 00000000..be0b1566 +--- /dev/null ++++ b/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 ++ÄÄÄ +diff --git a/tests/requests/invalid/nonascii_04.py b/tests/requests/invalid/nonascii_04.py +new file mode 100644 +index 00000000..d336fbc8 +--- /dev/null ++++ b/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 +diff --git a/tests/requests/invalid/prefix_01.http b/tests/requests/invalid/prefix_01.http +new file mode 100644 +index 00000000..f8bdeb35 +--- /dev/null ++++ b/tests/requests/invalid/prefix_01.http +@@ -0,0 +1,2 @@ ++GET\0PROXY /foo HTTP/1.1\r\n ++\r\n +diff --git a/tests/requests/invalid/prefix_01.py b/tests/requests/invalid/prefix_01.py +new file mode 100644 +index 00000000..86a0774e +--- /dev/null ++++ b/tests/requests/invalid/prefix_01.py +@@ -0,0 +1,2 @@ ++from gunicorn.http.errors import InvalidRequestMethod ++request = InvalidRequestMethod +\ No newline at end of file +diff --git a/tests/requests/invalid/prefix_02.http b/tests/requests/invalid/prefix_02.http +new file mode 100644 +index 00000000..8a9b155c +--- /dev/null ++++ b/tests/requests/invalid/prefix_02.http +@@ -0,0 +1,2 @@ ++GET\0 /foo HTTP/1.1\r\n ++\r\n +diff --git a/tests/requests/invalid/prefix_02.py b/tests/requests/invalid/prefix_02.py +new file mode 100644 +index 00000000..86a0774e +--- /dev/null ++++ b/tests/requests/invalid/prefix_02.py +@@ -0,0 +1,2 @@ ++from gunicorn.http.errors import InvalidRequestMethod ++request = InvalidRequestMethod +\ No newline at end of file +diff --git a/tests/requests/invalid/prefix_03.http b/tests/requests/invalid/prefix_03.http +new file mode 100644 +index 00000000..7803935c +--- /dev/null ++++ b/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 +diff --git a/tests/requests/invalid/prefix_03.py b/tests/requests/invalid/prefix_03.py +new file mode 100644 +index 00000000..95b0581a +--- /dev/null ++++ b/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 +diff --git a/tests/requests/invalid/prefix_04.http b/tests/requests/invalid/prefix_04.http +new file mode 100644 +index 00000000..712631c8 +--- /dev/null ++++ b/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 +diff --git a/tests/requests/invalid/prefix_04.py b/tests/requests/invalid/prefix_04.py +new file mode 100644 +index 00000000..95b0581a +--- /dev/null ++++ b/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 +diff --git a/tests/requests/invalid/prefix_05.http b/tests/requests/invalid/prefix_05.http +new file mode 100644 +index 00000000..120b6577 +--- /dev/null ++++ b/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 +diff --git a/tests/requests/invalid/prefix_05.py b/tests/requests/invalid/prefix_05.py +new file mode 100644 +index 00000000..0da10f42 +--- /dev/null ++++ b/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 +diff --git a/tests/requests/valid/025.http b/tests/requests/valid/025.http +index 62267add..f8d7fae2 100644 +--- a/tests/requests/valid/025.http ++++ b/tests/requests/valid/025.http +@@ -1,5 +1,4 @@ + 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 +@@ -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 +diff --git a/tests/requests/valid/025.py b/tests/requests/valid/025.py +index 12ea9ab7..33f5845c 100644 +--- a/tests/requests/valid/025.py ++++ b/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" +diff --git a/tests/requests/valid/025compat.http b/tests/requests/valid/025compat.http +new file mode 100644 +index 00000000..828f6fb7 +--- /dev/null ++++ b/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 +diff --git a/tests/requests/valid/025compat.py b/tests/requests/valid/025compat.py +new file mode 100644 +index 00000000..33f5845c +--- /dev/null ++++ b/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] +diff --git a/tests/requests/valid/029.http b/tests/requests/valid/029.http +index c8611dbd..5d029dd9 100644 +--- a/tests/requests/valid/029.http ++++ b/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 +diff --git a/tests/requests/valid/029.py b/tests/requests/valid/029.py +index f25449d1..64d02660 100644 +--- a/tests/requests/valid/029.py ++++ b/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" + } +diff --git a/tests/treq.py b/tests/treq.py +index ffe0691f..acfb9bb5 100644 +--- a/tests/treq.py ++++ b/tests/treq.py +@@ -246,8 +246,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): +-- +2.30.2 + diff -Nru gunicorn-20.1.0/debian/patches/0002-RFC-compliant-header-field-chunk-validation.patch gunicorn-20.1.0/debian/patches/0002-RFC-compliant-header-field-chunk-validation.patch --- gunicorn-20.1.0/debian/patches/0002-RFC-compliant-header-field-chunk-validation.patch 1970-01-01 00:00:00.000000000 +0000 +++ gunicorn-20.1.0/debian/patches/0002-RFC-compliant-header-field-chunk-validation.patch 2024-12-20 03:39:18.000000000 +0000 @@ -0,0 +1,59 @@ +From effe6b26097c7bc03fc59603c00d024034886812 Mon Sep 17 00:00:00 2001 +From: Ben Kallus +Date: Mon, 28 Aug 2023 22:32:36 -0400 +Subject: RFC compliant header field+chunk validation + +* update HEADER_RE and HEADER_VALUE_RE to match the RFCs +* update chunk length parsing to disallow 0x prefix and digit-separating underscores. +--- + gunicorn/http/body.py | 5 ++--- + gunicorn/http/message.py | 2 +- + gunicorn/http/wsgi.py | 2 +- + 3 files changed, 4 insertions(+), 5 deletions(-) + +diff --git a/gunicorn/http/body.py b/gunicorn/http/body.py +index afde3685..5bdd06ee 100644 +--- a/gunicorn/http/body.py ++++ b/gunicorn/http/body.py +@@ -86,10 +86,9 @@ class ChunkedReader(object): + 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: ++ 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: +diff --git a/gunicorn/http/message.py b/gunicorn/http/message.py +index 5018a188..bb8327f3 100644 +--- a/gunicorn/http/message.py ++++ b/gunicorn/http/message.py +@@ -22,7 +22,7 @@ MAX_REQUEST_LINE = 8190 + MAX_HEADERS = 32768 + DEFAULT_MAX_HEADERFIELD_SIZE = 8190 + +-HEADER_RE = re.compile(r"[\x00-\x1F\x7F()<>@,;:\[\]={} \t\\\"]") ++HEADER_RE = re.compile(r"[^!#$%&'*+\-.\^_`|~0-9a-zA-Z]") + METH_RE = re.compile(r"[A-Z0-9$-_.]{3,20}") + VERSION_RE = re.compile(r"HTTP/(\d+)\.(\d+)") + +diff --git a/gunicorn/http/wsgi.py b/gunicorn/http/wsgi.py +index 478677f4..83317875 100644 +--- a/gunicorn/http/wsgi.py ++++ b/gunicorn/http/wsgi.py +@@ -18,7 +18,7 @@ import gunicorn.util as util + # with sending files in blocks over 2GB. + BLKSIZE = 0x3FFFFFFF + +-HEADER_VALUE_RE = re.compile(r'[\x00-\x1F\x7F]') ++HEADER_VALUE_RE = re.compile(r'[^ \t\x21-\x7e\x80-\xff]') + + log = logging.getLogger(__name__) + +-- +2.30.2 + diff -Nru gunicorn-20.1.0/debian/patches/0003-Disallow-empty-header-names.patch gunicorn-20.1.0/debian/patches/0003-Disallow-empty-header-names.patch --- gunicorn-20.1.0/debian/patches/0003-Disallow-empty-header-names.patch 1970-01-01 00:00:00.000000000 +0000 +++ gunicorn-20.1.0/debian/patches/0003-Disallow-empty-header-names.patch 2024-12-20 03:39:18.000000000 +0000 @@ -0,0 +1,25 @@ +From fcbb5107d54794cf3f5f6eebe72823380b1e1fe6 Mon Sep 17 00:00:00 2001 +From: Ben Kallus +Date: Mon, 4 Dec 2023 17:08:16 -0500 +Subject: Disallow empty header names. + +--- + gunicorn/http/message.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/gunicorn/http/message.py b/gunicorn/http/message.py +index bb8327f3..11d485de 100644 +--- a/gunicorn/http/message.py ++++ b/gunicorn/http/message.py +@@ -87,7 +87,7 @@ class Message(object): + # Parse initial header name : value pair. + curr = lines.pop(0) + header_length = len(curr) +- if curr.find(":") < 0: ++ if curr.find(":") <= 0: + raise InvalidHeader(curr.strip()) + name, value = curr.split(":", 1) + if self.cfg.strip_header_spaces: +-- +2.30.2 + diff -Nru gunicorn-20.1.0/debian/patches/0004-RFC-compliant-request-line-and-header-parsing.patch gunicorn-20.1.0/debian/patches/0004-RFC-compliant-request-line-and-header-parsing.patch --- gunicorn-20.1.0/debian/patches/0004-RFC-compliant-request-line-and-header-parsing.patch 1970-01-01 00:00:00.000000000 +0000 +++ gunicorn-20.1.0/debian/patches/0004-RFC-compliant-request-line-and-header-parsing.patch 2024-12-20 03:39:18.000000000 +0000 @@ -0,0 +1,316 @@ +From 49a0c27aba38f4e14ccd928474241063dd2e360d Mon Sep 17 00:00:00 2001 +From: Ben Kallus +Date: Wed, 6 Dec 2023 17:28:40 -0500 +Subject: RFC compliant request line and header parsing + +- Unify HEADER_RE and METH_RE +- Replace CRLF with SP during obs-fold processing (See RFC 9112 Section 5.2, last paragraph) +- Stop stripping header names. +- Remove HTAB in OWS in header values that use obs-fold (See RFC 9112 Section 5.2, last paragraph) +- Use fullmatch instead of search, which has problems with empty strings. (See GHSA-68xg-gqqm-vgj8) +- Split proxy protocol line on space only. (See proxy protocol Section 2.1, bullet 3) +- Use fullmatch for method and version (Thank you to Paul Dorn for noticing this.) +- Replace calls to str.strip() with str.strip(' \t') +- Split request line on SP only. + +Co-authored-by: Paul Dorn +--- + gunicorn/http/message.py | 33 +++++++++-------- + gunicorn/http/wsgi.py | 23 ++++++------ + tests/requests/invalid/003.http | 4 +-- + tests/requests/invalid/003.py | 4 +-- + tests/requests/valid/016.py | 64 ++++++++++++++++----------------- + tests/requests/valid/031.http | 2 ++ + tests/requests/valid/031.py | 7 ++++ + 7 files changed, 74 insertions(+), 63 deletions(-) + create mode 100644 tests/requests/valid/031.http + create mode 100644 tests/requests/valid/031.py + +diff --git a/gunicorn/http/message.py b/gunicorn/http/message.py +index 11d485de..96f7a9ea 100644 +--- a/gunicorn/http/message.py ++++ b/gunicorn/http/message.py +@@ -22,8 +22,7 @@ MAX_REQUEST_LINE = 8190 + MAX_HEADERS = 32768 + DEFAULT_MAX_HEADERFIELD_SIZE = 8190 + +-HEADER_RE = re.compile(r"[^!#$%&'*+\-.\^_`|~0-9a-zA-Z]") +-METH_RE = re.compile(r"[A-Z0-9$-_.]{3,20}") ++TOKEN_RE = re.compile(r"[!#$%&'*+\-.\^_`|~0-9a-zA-Z]+") + VERSION_RE = re.compile(r"HTTP/(\d+)\.(\d+)") + + +@@ -67,8 +66,8 @@ class Message(object): + 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 +@@ -84,30 +83,30 @@ 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) ++ header_length = len(curr) + len("\r\n") + if curr.find(":") <= 0: +- raise InvalidHeader(curr.strip()) ++ 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): ++ if not TOKEN_RE.fullmatch(name): + raise InvalidHeaderName(name) + +- name, value = name.strip(), [value.lstrip()] ++ 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") +@@ -197,7 +196,7 @@ class Message(object): + 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": +@@ -324,7 +323,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) +@@ -369,12 +368,12 @@ 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]): ++ if not TOKEN_RE.fullmatch(bits[0]): + raise InvalidRequestMethod(bits[0]) + self.method = bits[0].upper() + +@@ -390,7 +389,7 @@ 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))) +diff --git a/gunicorn/http/wsgi.py b/gunicorn/http/wsgi.py +index 83317875..359cb27b 100644 +--- a/gunicorn/http/wsgi.py ++++ b/gunicorn/http/wsgi.py +@@ -9,7 +9,7 @@ import os + import re + import sys + +-from gunicorn.http.message import HEADER_RE ++from gunicorn.http.message import TOKEN_RE + from gunicorn.http.errors import 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'[^ \t\x21-\x7e\x80-\xff]') ++# 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__) + +@@ -249,31 +251,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 +diff --git a/tests/requests/invalid/003.http b/tests/requests/invalid/003.http +index cd1ab7fc..5a9eaafc 100644 +--- a/tests/requests/invalid/003.http ++++ b/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 +diff --git a/tests/requests/invalid/003.py b/tests/requests/invalid/003.py +index 86a0774e..5a4ca896 100644 +--- a/tests/requests/invalid/003.py ++++ b/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 +diff --git a/tests/requests/valid/016.py b/tests/requests/valid/016.py +index 139b2700..4e5144f8 100644 +--- a/tests/requests/valid/016.py ++++ b/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", +diff --git a/tests/requests/valid/031.http b/tests/requests/valid/031.http +new file mode 100644 +index 00000000..cd1ab7fc +--- /dev/null ++++ b/tests/requests/valid/031.http +@@ -0,0 +1,2 @@ ++-blargh /foo HTTP/1.1\r\n ++\r\n +\ No newline at end of file +diff --git a/tests/requests/valid/031.py b/tests/requests/valid/031.py +new file mode 100644 +index 00000000..9691a002 +--- /dev/null ++++ b/tests/requests/valid/031.py +@@ -0,0 +1,7 @@ ++request = { ++ "method": "-BLARGH", ++ "uri": uri("/foo"), ++ "version": (1, 1), ++ "headers": [], ++ "body": b"" ++} +-- +2.30.2 + diff -Nru gunicorn-20.1.0/debian/patches/0005-pytest-raise-on-malformed-test-fixtures.patch gunicorn-20.1.0/debian/patches/0005-pytest-raise-on-malformed-test-fixtures.patch --- gunicorn-20.1.0/debian/patches/0005-pytest-raise-on-malformed-test-fixtures.patch 1970-01-01 00:00:00.000000000 +0000 +++ gunicorn-20.1.0/debian/patches/0005-pytest-raise-on-malformed-test-fixtures.patch 2024-12-20 03:39:18.000000000 +0000 @@ -0,0 +1,57 @@ +From 97eb7a2d55851e6f720a64dc27ba6d3dbee63de9 Mon Sep 17 00:00:00 2001 +From: "Paul J. Dorn" +Date: Wed, 6 Dec 2023 15:30:50 +0100 +Subject: pytest: raise on malformed test fixtures + +and unbreak test depending on backslash escape +--- + tests/treq.py | 15 +++++++++++---- + 1 file changed, 11 insertions(+), 4 deletions(-) + +diff --git a/tests/treq.py b/tests/treq.py +index acfb9bb5..aeaae151 100644 +--- a/tests/treq.py ++++ b/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 +@@ -264,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) +@@ -272,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): +@@ -285,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 +-- +2.30.2 + diff -Nru gunicorn-20.1.0/debian/patches/series gunicorn-20.1.0/debian/patches/series --- gunicorn-20.1.0/debian/patches/series 2022-10-31 17:36:51.000000000 +0000 +++ gunicorn-20.1.0/debian/patches/series 2024-12-20 03:42:53.000000000 +0000 @@ -3,3 +3,8 @@ 0003-Don-t-call-chown-2-if-it-would-be-a-no-op.patch 0004-Set-supplementary-groups-when-changing-uid.patch 0005-eventlet-worker-ALREADY_HANDLED-WSGI_LOCAL.patch +0001-fail-safe-on-unsupported-request-framing.patch +0002-RFC-compliant-header-field-chunk-validation.patch +0003-Disallow-empty-header-names.patch +0004-RFC-compliant-request-line-and-header-parsing.patch +0005-pytest-raise-on-malformed-test-fixtures.patch