From 22b067793cbcd0fb5dee04cfd9115fa85a7ca110 Mon Sep 17 00:00:00 2001
From: Tom Most <twm@freecog.net>
Date: Sat, 5 Mar 2022 23:26:55 -0800
Subject: [PATCH 01/13] Some tests for GHSA-c2jg-hw38-jrqq

---
 src/twisted/web/test/test_http.py | 102 ++++++++++++++++++++++++++++++
 1 file changed, 102 insertions(+)

diff --git a/src/twisted/web/test/test_http.py b/src/twisted/web/test/test_http.py
index 7ffea4e0bc0..b4139558724 100644
--- a/src/twisted/web/test/test_http.py
+++ b/src/twisted/web/test/test_http.py
@@ -1751,6 +1751,56 @@ def process(self):
             [b"t \ta \tb"],
         )
 
+    def test_headerStripWhitespace(self):
+        """
+        Leading and trailing space and tab characters are stripped from
+        headers. Other forms of whitespace are preserved.
+
+        See RFC 7230 section 3.2.3 and 3.2.4.
+        """
+        processed = []
+
+        class MyRequest(http.Request):
+            def process(self):
+                processed.append(self)
+                self.finish()
+
+        requestLines = [
+            b"GET / HTTP/1.0",
+            b"spaces:   spaces were stripped   ",
+            b"tabs: \t\ttabs were stripped\t\t",
+            b"spaces-and-tabs: \t \t spaces and tabs were stripped\t \t",
+            b"line-tab:   \v vertical tab was preserved\v\t",
+            b"form-feed: \f form feed was preserved \f  ",
+            b"",
+            b"",
+        ]
+
+        self.runRequest(b"\n".join(requestLines), MyRequest, 0)
+        [request] = processed
+        # All leading and trailing whitespace is stripped from the
+        # header-value.
+        self.assertEqual(
+            request.requestHeaders.getRawHeaders(b"spaces"),
+            [b"spaces were stripped"],
+        )
+        self.assertEqual(
+            request.requestHeaders.getRawHeaders(b"tabs"),
+            [b"tabs were stripped"],
+        )
+        self.assertEqual(
+            request.requestHeaders.getRawHeaders(b"spaces-and-tabs"),
+            [b"spaces and tabs were stripped"],
+        )
+        self.assertEqual(
+            request.requestHeaders.getRawHeaders(b"line-tab"),
+            [b"\v vertical tab was preserved\v"],
+        )
+        self.assertEqual(
+            request.requestHeaders.getRawHeaders(b"form-feed"),
+            [b"\f form feed was preserved \f"],
+        )
+
     def test_tooManyHeaders(self):
         """
         C{HTTPChannel} enforces a limit of C{HTTPChannel.maxHeaders} on the
@@ -2315,6 +2365,58 @@ def test_duplicateContentLengths(self):
             ]
         )
 
+    def test_contentLengthMalformed(self):
+        """
+        A request with a non-integer C{Content-Length} header fails with a 400
+        response without calling L{Request.process}.
+        """
+        self.assertRequestRejected(
+            [
+                b"GET /a HTTP/1.1",
+                b"Content-Length: MORE THAN NINE THOUSAND!",
+                b"Host: host.invalid",
+                b"",
+                b"",
+                b"x" * 9001,
+            ]
+        )
+
+    def test_contentLengthTooPositive(self):
+        """
+        A request with a C{Content-Length} header that begins with a L{+} fails
+        with a 400 response without calling L{Request.process}.
+
+        This is a potential request smuggling vector: see GHSA-c2jg-hw38-jrqq.
+        """
+        self.assertRequestRejected(
+            [
+                b"GET /a HTTP/1.1",
+                b"Content-Length: +100",
+                b"Host: host.invalid",
+                b"",
+                b"",
+                b"x" * 100,
+            ]
+        )
+
+    def test_contentLengthNegative(self):
+        """
+        A request with a C{Content-Length} header that is negative fails with
+        a 400 response without calling L{Request.process}.
+
+        This is a potential request smuggling vector: see GHSA-c2jg-hw38-jrqq.
+        """
+        self.assertRequestRejected(
+            [
+                b"GET /a HTTP/1.1",
+                b"Content-Length: -100",
+                b"Host: host.invalid",
+                b"",
+                b"",
+                b"x" * 200,
+            ]
+        )
+
     def test_duplicateContentLengthsWithPipelinedRequests(self):
         """
         Two pipelined requests, the first of which includes multiple

From 79ee8c564ca0d4c2910c8859e0a6014d2dc40005 Mon Sep 17 00:00:00 2001
From: Tom Most <twm@freecog.net>
Date: Mon, 7 Mar 2022 00:02:55 -0800
Subject: [PATCH 02/13] Replace obs-fold with a single space

---
 src/twisted/web/http.py           |  2 +-
 src/twisted/web/test/test_http.py | 13 +++++++++----
 2 files changed, 10 insertions(+), 5 deletions(-)

diff --git a/src/twisted/web/http.py b/src/twisted/web/http.py
index a53ebc27c28..ce9b796404c 100644
--- a/src/twisted/web/http.py
+++ b/src/twisted/web/http.py
@@ -2246,7 +2246,7 @@ def lineReceived(self, line):
                 self.setRawMode()
         elif line[0] in b" \t":
             # Continuation of a multi line header.
-            self.__header = self.__header + b"\n" + line
+            self.__header += b" " + line.lstrip(b" \t")
         # Regular header line.
         # Processing of header line is delayed to allow accumulating multi
         # line headers.
diff --git a/src/twisted/web/test/test_http.py b/src/twisted/web/test/test_http.py
index b4139558724..91258f8f51b 100644
--- a/src/twisted/web/test/test_http.py
+++ b/src/twisted/web/test/test_http.py
@@ -1703,7 +1703,12 @@ def test_headersMultiline(self):
         Line folded headers are handled by L{HTTPChannel} by replacing each
         fold with a single space by the time they are made available to the
         L{Request}. Any leading whitespace in the folded lines of the header
-        value is preserved.
+        value is replaced with a single space, per:
+
+            A server that receives an obs-fold in a request message ... MUST
+            ... replace each received obs-fold with one or more SP octets prior
+            to interpreting the field value or forwarding the message
+            downstream.
 
         See RFC 7230 section 3.2.4.
         """
@@ -1740,15 +1745,15 @@ def process(self):
         )
         self.assertEqual(
             request.requestHeaders.getRawHeaders(b"space"),
-            [b"space  space"],
+            [b"space space"],
         )
         self.assertEqual(
             request.requestHeaders.getRawHeaders(b"spaces"),
-            [b"spaces   spaces    spaces"],
+            [b"spaces spaces spaces"],
         )
         self.assertEqual(
             request.requestHeaders.getRawHeaders(b"tab"),
-            [b"t \ta \tb"],
+            [b"t a b"],
         )
 
     def test_headerStripWhitespace(self):

From c3a4e1d015740c1d87a3ec7d57570257e75b0062 Mon Sep 17 00:00:00 2001
From: Tom Most <twm@freecog.net>
Date: Mon, 7 Mar 2022 00:03:50 -0800
Subject: [PATCH 03/13] Strip only spaces and tabs from header values

---
 src/twisted/web/http.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/twisted/web/http.py b/src/twisted/web/http.py
index ce9b796404c..b599b7c543c 100644
--- a/src/twisted/web/http.py
+++ b/src/twisted/web/http.py
@@ -2327,7 +2327,7 @@ def headerReceived(self, line):
             return False
 
         header = header.lower()
-        data = data.strip()
+        data = data.strip(b" \t")
 
         if not self._maybeChooseTransferDecoder(header, data):
             return False

From 8ebfa8f6577431226e109ff98ba48f5152a2c416 Mon Sep 17 00:00:00 2001
From: Tom Most <twm@freecog.net>
Date: Mon, 7 Mar 2022 00:32:14 -0800
Subject: [PATCH 04/13] Reject non-digit Content-Length

---
 src/twisted/web/http.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/twisted/web/http.py b/src/twisted/web/http.py
index b599b7c543c..a551e33daec 100644
--- a/src/twisted/web/http.py
+++ b/src/twisted/web/http.py
@@ -2274,6 +2274,8 @@ def fail():
 
         # Can this header determine the length?
         if header == b"content-length":
+            if not data.isdigit():
+                return fail()
             try:
                 length = int(data)
             except ValueError:

From f22d0d9c889822adb7eaf84b42a20ff5f7c4d421 Mon Sep 17 00:00:00 2001
From: Tom Most <twm@freecog.net>
Date: Sun, 13 Mar 2022 23:19:39 -0700
Subject: [PATCH 05/13] Test for malformed chunk size and extensions

---
 src/twisted/web/test/test_http.py | 34 +++++++++++++++++++++++++++++++
 1 file changed, 34 insertions(+)

diff --git a/src/twisted/web/test/test_http.py b/src/twisted/web/test/test_http.py
index 91258f8f51b..d6362d2dd29 100644
--- a/src/twisted/web/test/test_http.py
+++ b/src/twisted/web/test/test_http.py
@@ -1279,6 +1279,22 @@ def test_extensions(self):
         p.dataReceived(b"3; x-foo=bar\r\nabc\r\n")
         self.assertEqual(L, [b"abc"])
 
+    def test_extensionsMalformed(self):
+        """
+        L{_ChunkedTransferDecoder.dataReceived} raises
+        L{_MalformedChunkedDataError} when the chunk extension fields contain
+        invalid characters.
+
+        This is a potential request smuggling vector: see GHSA-c2jg-hw38-jrqq.
+        """
+        for b in [*range(0, 0x09), *range(0x10, 0x21), *range(0x74, 0x80)]:
+            data = b"3; " + bytes((b,)) + b"\r\nabc\r\n"
+            p = http._ChunkedTransferDecoder(
+                lambda b: None,  # pragma: nocov
+                lambda b: None,  # pragma: nocov
+            )
+            self.assertRaises(http._MalformedChunkedDataError, p.dataReceived, data)
+
     def test_oversizedChunkSizeLine(self):
         """
         L{_ChunkedTransferDecoder.dataReceived} raises
@@ -1334,6 +1350,22 @@ def test_malformedChunkSizeNegative(self):
             http._MalformedChunkedDataError, p.dataReceived, b"-3\r\nabc\r\n"
         )
 
+    def test_malformedChunkSizeHex(self):
+        """
+        L{_ChunkedTransferDecoder.dataReceived} raises
+        L{_MalformedChunkedDataError} when the chunk size is prefixed with
+        "0x", as if it were a Python integer literal.
+
+        This is a potential request smuggling vector: see GHSA-c2jg-hw38-jrqq.
+        """
+        p = http._ChunkedTransferDecoder(
+            lambda b: None,  # pragma: nocov
+            lambda b: None,  # pragma: nocov
+        )
+        self.assertRaises(
+            http._MalformedChunkedDataError, p.dataReceived, b"0x3\r\nabc\r\n"
+        )
+
     def test_malformedChunkEnd(self):
         r"""
         L{_ChunkedTransferDecoder.dataReceived} raises
@@ -1446,6 +1478,8 @@ def testChunks(self):
             chunked = b"".join(http.toChunk(s))
             self.assertEqual((s, b""), http.fromChunk(chunked))
         self.assertRaises(ValueError, http.fromChunk, b"-5\r\nmalformed!\r\n")
+        self.assertRaises(ValueError, http.fromChunk, b"0xa\r\nmalformed!\r\n")
+        self.assertRaises(ValueError, http.fromChunk, b"0XA\r\nmalformed!\r\n")
 
     def testConcatenatedChunks(self):
         chunked = b"".join([b"".join(http.toChunk(t)) for t in self.strings])

From 0275152f147506c82868ff1dabd9bf655ab67946 Mon Sep 17 00:00:00 2001
From: Tom Most <twm@freecog.net>
Date: Sun, 13 Mar 2022 23:51:52 -0700
Subject: [PATCH 06/13] Reject malformed chunk sizes

---
 src/twisted/web/http.py           | 35 +++++++++++++++++++++++----
 src/twisted/web/test/test_http.py | 40 +++++++++++++++++++++++++++++++
 2 files changed, 71 insertions(+), 4 deletions(-)

diff --git a/src/twisted/web/http.py b/src/twisted/web/http.py
index a551e33daec..b089fa7c5ed 100644
--- a/src/twisted/web/http.py
+++ b/src/twisted/web/http.py
@@ -108,7 +108,7 @@
 import time
 import warnings
 from io import BytesIO
-from typing import AnyStr, Callable, Optional
+from typing import AnyStr, Callable, Optional, Tuple
 from urllib.parse import (
     ParseResultBytes,
     unquote_to_bytes as unquote,
@@ -410,7 +410,33 @@ def toChunk(data):
     return (networkString(f"{len(data):x}"), b"\r\n", data, b"\r\n")
 
 
-def fromChunk(data):
+def _ishexdigits(b: bytes) -> bool:
+    """
+    Is the string case-insensitively hexidecimal?
+
+    It must be composed of one or more characters in the ranges a-f, A-F
+    and 0-9.
+    """
+    for c in b:
+        if c not in b'0123456789abcdefABCDEF':
+            return False
+    return bool(b)
+
+
+def _hexint(b: bytes) -> int:
+    """
+    Decode a hexadecimal integer.
+
+    Unlike L{int(b, 16)}, this raises L{ValueError} when the integer has
+    a prefix like C{b'0x'}, C{b'+'}, or C{b'-'}, which is desirable when
+    parsing network protocols.
+    """
+    if not _ishexdigits(b):
+        raise ValueError(b)
+    return int(b, 16)
+
+
+def fromChunk(data: bytes) -> Tuple[bytes, bytes]:
     """
     Convert chunk to string.
 
@@ -422,7 +448,7 @@ def fromChunk(data):
         byte string.
     """
     prefix, rest = data.split(b"\r\n", 1)
-    length = int(prefix, 16)
+    length = _hexint(prefix)
     if length < 0:
         raise ValueError("Chunk length must be >= 0, not %d" % (length,))
     if rest[length : length + 2] != b"\r\n":
@@ -1883,8 +1909,9 @@ def _dataReceived_CHUNK_LENGTH(self) -> bool:
         endOfLengthIndex = self._buffer.find(b";", 0, eolIndex)
         if endOfLengthIndex == -1:
             endOfLengthIndex = eolIndex
+        rawLength = self._buffer[0:endOfLengthIndex]
         try:
-            length = int(self._buffer[0:endOfLengthIndex], 16)
+            length = _hexint(rawLength)
         except ValueError:
             raise _MalformedChunkedDataError("Chunk-size must be an integer.")
 
diff --git a/src/twisted/web/test/test_http.py b/src/twisted/web/test/test_http.py
index d6362d2dd29..6948dcadc0c 100644
--- a/src/twisted/web/test/test_http.py
+++ b/src/twisted/web/test/test_http.py
@@ -4380,3 +4380,43 @@ def test_sendHeaderSanitizesLinearWhitespace(self):
                 transport.value().splitlines(),
                 [b": ".join([sanitizedBytes, sanitizedBytes])],
             )
+
+
+class HexHelperTests(unittest.SynchronousTestCase):
+    """
+    Test the L{http._hexint} and L{http._ishexdigits} helper functions.
+    """
+
+    badStrings = (b"", b"0x1234", b"feds", b"-123" b"+123")
+
+    def test_isHex(self):
+        """
+        L{_ishexdigits()} returns L{True} for nonempy bytestrings containing
+        hexadecimal digits.
+        """
+        for s in (b"10", b"abcdef", b"AB1234", b"fed", b"123467890"):
+            self.assertIs(True, http._ishexdigits(s))
+
+    def test_decodes(self):
+        """
+        L{_hexint()} returns the integer equivalent of the input.
+        """
+        self.assertEqual(10, http._hexint(b"a"))
+        self.assertEqual(0x10, http._hexint(b"10"))
+        self.assertEqual(0xABCD123, http._hexint(b"abCD123"))
+
+    def test_isNotHex(self):
+        """
+        L{_ishexdigits()} returns L{False} for bytestrings that don't contain
+        hexadecimal digits, including the empty string.
+        """
+        for s in self.badStrings:
+            self.assertIs(False, http._ishexdigits(s))
+
+    def test_decodeNotHex(self):
+        """
+        L{_hexint()} raises L{ValueError} for bytestrings that can't
+        be decoded.
+        """
+        for s in self.badStrings:
+            self.assertRaises(ValueError, http._hexint, s)

From 2a5763d5b168372abb591c0eb6323ed4dfe8a4fc Mon Sep 17 00:00:00 2001
From: Tom Most <twm@freecog.net>
Date: Sun, 13 Mar 2022 23:55:26 -0700
Subject: [PATCH 07/13] We should deprecate http.fromChunk

---
 src/twisted/web/http.py | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/twisted/web/http.py b/src/twisted/web/http.py
index b089fa7c5ed..46058f61518 100644
--- a/src/twisted/web/http.py
+++ b/src/twisted/web/http.py
@@ -440,6 +440,9 @@ def fromChunk(data: bytes) -> Tuple[bytes, bytes]:
     """
     Convert chunk to string.
 
+    Note that this function is not specification compliant: it doesn't handle
+    chunk extensions.
+
     @type data: C{bytes}
 
     @return: tuple of (result, remaining) - both C{bytes}.

From 696bfeaf5a1fa7ff952f860c89e2bdcfacef7d7a Mon Sep 17 00:00:00 2001
From: Tom Most <twm@freecog.net>
Date: Sun, 13 Mar 2022 23:57:23 -0700
Subject: [PATCH 08/13] Remove unreachable branch

---
 src/twisted/web/http.py | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/src/twisted/web/http.py b/src/twisted/web/http.py
index 46058f61518..32040fb8fd3 100644
--- a/src/twisted/web/http.py
+++ b/src/twisted/web/http.py
@@ -1918,9 +1918,7 @@ def _dataReceived_CHUNK_LENGTH(self) -> bool:
         except ValueError:
             raise _MalformedChunkedDataError("Chunk-size must be an integer.")
 
-        if length < 0:
-            raise _MalformedChunkedDataError("Chunk-size must not be negative.")
-        elif length == 0:
+        if length == 0:
             self.state = "TRAILER"
         else:
             self.state = "BODY"

From fa9caa54d63399b4ccdfbf0429ba1b504ccc7c89 Mon Sep 17 00:00:00 2001
From: Tom Most <twm@freecog.net>
Date: Sun, 27 Mar 2022 22:17:30 -0700
Subject: [PATCH 09/13] Correct chunk extension byte validation

Go back to the RFC to figure out the correct allowed ranges.
---
 src/twisted/web/http.py           | 49 ++++++++++++++++++++++++++++++-
 src/twisted/web/test/test_http.py |  8 ++++-
 2 files changed, 55 insertions(+), 2 deletions(-)

diff --git a/src/twisted/web/http.py b/src/twisted/web/http.py
index 32040fb8fd3..e8327f03f2c 100644
--- a/src/twisted/web/http.py
+++ b/src/twisted/web/http.py
@@ -418,7 +418,7 @@ def _ishexdigits(b: bytes) -> bool:
     and 0-9.
     """
     for c in b:
-        if c not in b'0123456789abcdefABCDEF':
+        if c not in b"0123456789abcdefABCDEF":
             return False
     return bool(b)
 
@@ -1819,6 +1819,47 @@ def noMoreData(self):
 maxChunkSizeLineLength = 1024
 
 
+_chunkExtChars = (
+    b"\t !\"#$%&'()*+,-./0123456789:;<=>?@"
+    b"ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`"
+    b"abcdefghijklmnopqrstuvwxyz{|}~"
+    b"\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f"
+    b"\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f"
+    b"\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf"
+    b"\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf"
+    b"\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf"
+    b"\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf"
+    b"\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef"
+    b"\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"
+)
+"""
+Characters that are valid in a chunk extension.
+
+See RFC 7230 section 4.1.1:
+
+     chunk-ext      = *( ";" chunk-ext-name [ "=" chunk-ext-val ] )
+
+     chunk-ext-name = token
+     chunk-ext-val  = token / quoted-string
+
+Section 3.2.6:
+
+     token          = 1*tchar
+
+     tchar          = "!" / "#" / "$" / "%" / "&" / "'" / "*"
+                    / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
+                    / DIGIT / ALPHA
+                    ; any VCHAR, except delimiters
+
+     quoted-string  = DQUOTE *( qdtext / quoted-pair ) DQUOTE
+     qdtext         = HTAB / SP /%x21 / %x23-5B / %x5D-7E / obs-text
+     obs-text       = %x80-FF
+
+We don't check if chunk extensions are well-formed beyond validating that they
+don't contain characters outside this range.
+"""
+
+
 class _ChunkedTransferDecoder:
     """
     Protocol for decoding I{chunked} Transfer-Encoding, as defined by RFC 7230,
@@ -1918,6 +1959,12 @@ def _dataReceived_CHUNK_LENGTH(self) -> bool:
         except ValueError:
             raise _MalformedChunkedDataError("Chunk-size must be an integer.")
 
+        ext = self._buffer[endOfLengthIndex + 1 : eolIndex]
+        if ext and ext.translate(None, _chunkExtChars) != b"":
+            raise _MalformedChunkedDataError(
+                f"Invalid characters in chunk extensions: {ext!r}."
+            )
+
         if length == 0:
             self.state = "TRAILER"
         else:
diff --git a/src/twisted/web/test/test_http.py b/src/twisted/web/test/test_http.py
index 6948dcadc0c..f304991ca48 100644
--- a/src/twisted/web/test/test_http.py
+++ b/src/twisted/web/test/test_http.py
@@ -1287,7 +1287,13 @@ def test_extensionsMalformed(self):
 
         This is a potential request smuggling vector: see GHSA-c2jg-hw38-jrqq.
         """
-        for b in [*range(0, 0x09), *range(0x10, 0x21), *range(0x74, 0x80)]:
+        invalidControl = (
+            b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\n\x0b\x0c\r\x0e\x0f"
+            b"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"
+        )
+        invalidDelimiter = b"\\"
+        invalidDel = b"\x7f"
+        for b in invalidControl + invalidDelimiter + invalidDel:
             data = b"3; " + bytes((b,)) + b"\r\nabc\r\n"
             p = http._ChunkedTransferDecoder(
                 lambda b: None,  # pragma: nocov

From c6d6a3ea2a819a99f17b54b08b7fd2c75587a8a7 Mon Sep 17 00:00:00 2001
From: Tom Most <twm@freecog.net>
Date: Wed, 30 Mar 2022 23:16:20 -0700
Subject: [PATCH 10/13] Add newsfragment

---
 src/twisted/web/newsfragments/10323.bugfix | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 src/twisted/web/newsfragments/10323.bugfix

diff --git a/src/twisted/web/newsfragments/10323.bugfix b/src/twisted/web/newsfragments/10323.bugfix
new file mode 100644
index 00000000000..6aad08919b4
--- /dev/null
+++ b/src/twisted/web/newsfragments/10323.bugfix
@@ -0,0 +1 @@
+Correct several defects in HTTP request parsing that could permit HTTP request smugling: disallow signed Content-Length headers, forbid illegal characters in chunked extensions, forbid 0x prefix to chunk lengths, and only strip space and horizontal tab from header values. GHSA-c2jg-hw38-jrqq

From fbb001e9a66bdc97507e930388a7999fca323706 Mon Sep 17 00:00:00 2001
From: Tom Most <twm@freecog.net>
Date: Thu, 31 Mar 2022 21:47:23 -0700
Subject: [PATCH 11/13] Add CVE to the newsfragment

---
 src/twisted/web/newsfragments/10323.bugfix | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/twisted/web/newsfragments/10323.bugfix b/src/twisted/web/newsfragments/10323.bugfix
index 6aad08919b4..a123aa89862 100644
--- a/src/twisted/web/newsfragments/10323.bugfix
+++ b/src/twisted/web/newsfragments/10323.bugfix
@@ -1 +1 @@
-Correct several defects in HTTP request parsing that could permit HTTP request smugling: disallow signed Content-Length headers, forbid illegal characters in chunked extensions, forbid 0x prefix to chunk lengths, and only strip space and horizontal tab from header values. GHSA-c2jg-hw38-jrqq
+Correct several defects in HTTP request parsing that could permit HTTP request smugling: disallow signed Content-Length headers, forbid illegal characters in chunked extensions, forbid 0x prefix to chunk lengths, and only strip space and horizontal tab from header values. Addresses CVE-2022-24801 and GHSA-c2jg-hw38-jrqq.

From 2bbd6c89110f0d44d2bb109c14d787f65bca9df8 Mon Sep 17 00:00:00 2001
From: Tom Most <twm@freecog.net>
Date: Fri, 1 Apr 2022 20:47:59 -0700
Subject: [PATCH 12/13] Address review feedback

---
 src/twisted/web/http.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/twisted/web/http.py b/src/twisted/web/http.py
index e8327f03f2c..b80a55a2b00 100644
--- a/src/twisted/web/http.py
+++ b/src/twisted/web/http.py
@@ -420,7 +420,7 @@ def _ishexdigits(b: bytes) -> bool:
     for c in b:
         if c not in b"0123456789abcdefABCDEF":
             return False
-    return bool(b)
+    return b != b""
 
 
 def _hexint(b: bytes) -> int:
@@ -1835,14 +1835,14 @@ def noMoreData(self):
 """
 Characters that are valid in a chunk extension.
 
-See RFC 7230 section 4.1.1:
+See RFC 7230 section 4.1.1::
 
      chunk-ext      = *( ";" chunk-ext-name [ "=" chunk-ext-val ] )
 
      chunk-ext-name = token
      chunk-ext-val  = token / quoted-string
 
-Section 3.2.6:
+And section 3.2.6::
 
      token          = 1*tchar
 

From a5138047c6bccb23eae2c1ecf4fa2fe17890872c Mon Sep 17 00:00:00 2001
From: Tom Most <twm@freecog.net>
Date: Fri, 1 Apr 2022 21:38:48 -0700
Subject: [PATCH 13/13] Revise newsfragment

---
 src/twisted/web/newsfragments/10323.bugfix | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/twisted/web/newsfragments/10323.bugfix b/src/twisted/web/newsfragments/10323.bugfix
index a123aa89862..1890b1b58e1 100644
--- a/src/twisted/web/newsfragments/10323.bugfix
+++ b/src/twisted/web/newsfragments/10323.bugfix
@@ -1 +1 @@
-Correct several defects in HTTP request parsing that could permit HTTP request smugling: disallow signed Content-Length headers, forbid illegal characters in chunked extensions, forbid 0x prefix to chunk lengths, and only strip space and horizontal tab from header values. Addresses CVE-2022-24801 and GHSA-c2jg-hw38-jrqq.
+twisted.web.http had several several defects in HTTP request parsing that could permit HTTP request smuggling. It now disallows signed Content-Length headers, forbids illegal characters in chunked extensions, forbids 0x prefix to chunk lengths, and only strips spaces and horizontal tab characters from header values. These changes address CVE-2022-24801 and GHSA-c2jg-hw38-jrqq.
