Bug#1090853: bookworm-pu: package gunicorn/20.1.0-6+deb12u1
Package: release.debian.org
Severity: normal
Tags: bookworm
User: release.debian.org@packages.debian.org
Usertags: pu
X-Debbugs-Cc: security@debian.org, Debian Python Team <team+python@tracker.debian.org>
* CVE-2024-1135: HTTP Request Smuggling (Closes: #1069126)
diffstat for gunicorn-20.1.0 gunicorn-20.1.0
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 19:36:51.000000000 +0200
+++ gunicorn-20.1.0/debian/changelog 2024-12-20 05:42:55.000000000 +0200
@@ -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 <bunk@debian.org> 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 02:00:00.000000000 +0200
+++ gunicorn-20.1.0/debian/patches/0001-fail-safe-on-unsupported-request-framing.patch 2024-12-20 05:39:18.000000000 +0200
@@ -0,0 +1,692 @@
+From f6f3c50e9cd5e0bffe3bec65d7f8690baab9c824 Mon Sep 17 00:00:00 2001
+From: "Paul J. Dorn" <pajod@users.noreply.github.com>
+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 02:00:00.000000000 +0200
+++ gunicorn-20.1.0/debian/patches/0002-RFC-compliant-header-field-chunk-validation.patch 2024-12-20 05:39:18.000000000 +0200
@@ -0,0 +1,59 @@
+From effe6b26097c7bc03fc59603c00d024034886812 Mon Sep 17 00:00:00 2001
+From: Ben Kallus <benjamin.p.kallus.gr@dartmouth.edu>
+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 02:00:00.000000000 +0200
+++ gunicorn-20.1.0/debian/patches/0003-Disallow-empty-header-names.patch 2024-12-20 05:39:18.000000000 +0200
@@ -0,0 +1,25 @@
+From fcbb5107d54794cf3f5f6eebe72823380b1e1fe6 Mon Sep 17 00:00:00 2001
+From: Ben Kallus <benjamin.p.kallus.gr@dartmouth.edu>
+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 02:00:00.000000000 +0200
+++ gunicorn-20.1.0/debian/patches/0004-RFC-compliant-request-line-and-header-parsing.patch 2024-12-20 05:39:18.000000000 +0200
@@ -0,0 +1,316 @@
+From 49a0c27aba38f4e14ccd928474241063dd2e360d Mon Sep 17 00:00:00 2001
+From: Ben Kallus <benjamin.p.kallus.gr@dartmouth.edu>
+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 <pajod@users.noreply.github.com>
+---
+ 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 02:00:00.000000000 +0200
+++ gunicorn-20.1.0/debian/patches/0005-pytest-raise-on-malformed-test-fixtures.patch 2024-12-20 05:39:18.000000000 +0200
@@ -0,0 +1,57 @@
+From 97eb7a2d55851e6f720a64dc27ba6d3dbee63de9 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: 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 19:36:51.000000000 +0200
+++ gunicorn-20.1.0/debian/patches/series 2024-12-20 05:42:53.000000000 +0200
@@ -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
Reply to: