Package: release.debian.org
Severity: normal
Tags: bookworm
User: release.debian....@packages.debian.org
Usertags: pu
X-Debbugs-Cc: secur...@debian.org, Debian Python Team 
<team+pyt...@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 <b...@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" <pa...@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...@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...@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...@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 <pa...@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" <pa...@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 via email to