[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

Bug#1090853: marked as done (bookworm-pu: package gunicorn/20.1.0-6+deb12u1)



Your message dated Sat, 11 Jan 2025 11:03:09 +0000
with message-id <E1tWZGn-009jYe-03@coccia.debian.org>
and subject line Close 1090853
has caused the Debian Bug report #1090853,
regarding bookworm-pu: package gunicorn/20.1.0-6+deb12u1
to be marked as done.

This means that you claim that the problem has been dealt with.
If this is not the case it is now your responsibility to reopen the
Bug report if necessary, and/or fix the problem forthwith.

(NB: If you are a system administrator and have no idea what this
message is talking about, this may indicate a serious mail system
misconfiguration somewhere. Please contact owner@bugs.debian.org
immediately.)


-- 
1090853: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1090853
Debian Bug Tracking System
Contact owner@bugs.debian.org with problems
--- Begin Message ---
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

--- End Message ---
--- Begin Message ---
Version: 12.9
This update has been released as part of 12.9. Thank you for your contribution.

--- End Message ---

Reply to: