CVE-2020-10108
Description
In Twisted Web through 19.10.0, there was an HTTP request splitting vulnerability. When presented with two content-length headers, it ignored the first header. When the second content-length value was set to zero, the request body was interpreted as a pipelined request.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Twisted Web through 19.10.0 ignores the first Content-Length header; a second zero-length header enables HTTP request splitting.
Root
Cause
CVE-2020-10108 is an HTTP request splitting vulnerability in Twisted Web through version 19.10.0 [1]. The parser incorrectly handles multiple Content-Length headers: it discards the first header and uses the second. When an attacker sets the second Content-Length to zero, the request body is misinterpreted as the beginning of a pipelined (new) HTTP request [1][3].
Exploitation
An attacker can inject a crafted request with two Content-Length headers, the first containing a legitimate length and the second set to 0. The server then treats the request body of the first request as the start of a second, attacker-controlled request, effectively smuggling that second request past security controls [3]. No authentication is required; the attack is performed over HTTP.
Impact
Successful exploitation allows an attacker to perform request smuggling attacks, which can lead to cache poisoning, session hijacking via socket poisoning, and security filter bypasses [3]. The practical risk depends on how Twisted is deployed in the target environment.
Mitigation
The vulnerability is fixed in Twisted version 20.3.0rc1 [3]. Ubuntu published updates for their supported releases in USN-4308-1 and USN-4308-2 [2][4]. Affected users should update to the patched version or apply the vendor's workaround.
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
TwistedPyPI | < 20.3.0 | 20.3.0 |
Affected products
8- Twisted/Twisted Webdescription
- ghsa-coords7 versionspkg:pypi/twistedpkg:rpm/suse/python-Twisted&distro=HPE%20Helion%20OpenStack%208pkg:rpm/suse/python-Twisted&distro=SUSE%20Linux%20Enterprise%20Module%20for%20Web%20and%20Scripting%2012pkg:rpm/suse/python-Twisted&distro=SUSE%20OpenStack%20Cloud%208pkg:rpm/suse/python-Twisted&distro=SUSE%20OpenStack%20Cloud%209pkg:rpm/suse/python-Twisted&distro=SUSE%20OpenStack%20Cloud%20Crowbar%208pkg:rpm/suse/python-Twisted&distro=SUSE%20OpenStack%20Cloud%20Crowbar%209
< 20.3.0+ 6 more
- (no CPE)range: < 20.3.0
- (no CPE)range: < 15.2.1-9.20.1
- (no CPE)range: < 15.2.1-9.20.1
- (no CPE)range: < 15.2.1-9.20.1
- (no CPE)range: < 15.2.1-9.20.1
- (no CPE)range: < 15.2.1-9.20.1
- (no CPE)range: < 15.2.1-9.20.1
Patches
14a7d22e490bbFix several request smuggling attacks.
3 files changed · +187 −15
src/twisted/web/http.py+49 −15 modified@@ -2171,6 +2171,51 @@ def _finishRequestBody(self, data): self.allContentReceived() self._dataBuffer.append(data) + def _maybeChooseTransferDecoder(self, header, data): + """ + If the provided header is C{content-length} or + C{transfer-encoding}, choose the appropriate decoder if any. + + Returns L{True} if the request can proceed and L{False} if not. + """ + + def fail(): + self._respondToBadRequestAndDisconnect() + self.length = None + + # Can this header determine the length? + if header == b'content-length': + try: + length = int(data) + except ValueError: + fail() + return False + newTransferDecoder = _IdentityTransferDecoder( + length, self.requests[-1].handleContentChunk, self._finishRequestBody) + elif header == b'transfer-encoding': + # XXX Rather poorly tested code block, apparently only exercised by + # test_chunkedEncoding + if data.lower() == b'chunked': + length = None + newTransferDecoder = _ChunkedTransferDecoder( + self.requests[-1].handleContentChunk, self._finishRequestBody) + elif data.lower() == b'identity': + return True + else: + fail() + return False + else: + # It's not a length related header, so exit + return True + + if self._transferDecoder is not None: + fail() + return False + else: + self.length = length + self._transferDecoder = newTransferDecoder + return True + def headerReceived(self, line): """ @@ -2196,21 +2241,10 @@ def headerReceived(self, line): header = header.lower() data = data.strip() - if header == b'content-length': - try: - self.length = int(data) - except ValueError: - self._respondToBadRequestAndDisconnect() - self.length = None - return False - self._transferDecoder = _IdentityTransferDecoder( - self.length, self.requests[-1].handleContentChunk, self._finishRequestBody) - elif header == b'transfer-encoding' and data.lower() == b'chunked': - # XXX Rather poorly tested code block, apparently only exercised by - # test_chunkedEncoding - self.length = None - self._transferDecoder = _ChunkedTransferDecoder( - self.requests[-1].handleContentChunk, self._finishRequestBody) + + if not self._maybeChooseTransferDecoder(header, data): + return False + reqHeaders = self.requests[-1].requestHeaders values = reqHeaders.getRawHeaders(header) if values is not None:
src/twisted/web/newsfragments/9770.bugfix+1 −0 added@@ -0,0 +1 @@ +Fix several request smuggling attacks: requests with multiple Content-Length headers were allowed (thanks to Jake Miller from Bishop Fox and ZeddYu Lu) and now fail with a 400; requests with a Content-Length header and a Transfer-Encoding header honored the first header (thanks to Jake Miller from Bishop Fox) and now fail with a 400; requests whose Transfer-Encoding header had a value other than "chunked" and "identity" (thanks to ZeddYu Lu) were allowed and now fail a 400. \ No newline at end of file
src/twisted/web/test/test_http.py+137 −0 modified@@ -2252,6 +2252,143 @@ def process(self): self.flushLoggedErrors(AttributeError) + def assertDisconnectingBadRequest(self, request): + """ + Assert that the given request bytes fail with a 400 bad + request without calling L{Request.process}. + + @param request: A raw HTTP request + @type request: L{bytes} + """ + class FailedRequest(http.Request): + processed = False + def process(self): + FailedRequest.processed = True + + channel = self.runRequest(request, FailedRequest, success=False) + self.assertFalse(FailedRequest.processed, "Request.process called") + self.assertEqual( + channel.transport.value(), + b"HTTP/1.1 400 Bad Request\r\n\r\n") + self.assertTrue(channel.transport.disconnecting) + + + def test_duplicateContentLengths(self): + """ + A request which includes multiple C{content-length} headers + fails with a 400 response without calling L{Request.process}. + """ + self.assertRequestRejected([ + b'GET /a HTTP/1.1', + b'Content-Length: 56', + b'Content-Length: 0', + b'Host: host.invalid', + b'', + b'', + ]) + + + def test_duplicateContentLengthsWithPipelinedRequests(self): + """ + Two pipelined requests, the first of which includes multiple + C{content-length} headers, trigger a 400 response without + calling L{Request.process}. + """ + self.assertRequestRejected([ + b'GET /a HTTP/1.1', + b'Content-Length: 56', + b'Content-Length: 0', + b'Host: host.invalid', + b'', + b'', + b'GET /a HTTP/1.1', + b'Host: host.invalid', + b'', + b'', + ]) + + + def test_contentLengthAndTransferEncoding(self): + """ + A request that includes both C{content-length} and + C{transfer-encoding} headers fails with a 400 response without + calling L{Request.process}. + """ + self.assertRequestRejected([ + b'GET /a HTTP/1.1', + b'Transfer-Encoding: chunked', + b'Content-Length: 0', + b'Host: host.invalid', + b'', + b'', + ]) + + + def test_contentLengthAndTransferEncodingWithPipelinedRequests(self): + """ + Two pipelined requests, the first of which includes both + C{content-length} and C{transfer-encoding} headers, triggers a + 400 response without calling L{Request.process}. + """ + self.assertRequestRejected([ + b'GET /a HTTP/1.1', + b'Transfer-Encoding: chunked', + b'Content-Length: 0', + b'Host: host.invalid', + b'', + b'', + b'GET /a HTTP/1.1', + b'Host: host.invalid', + b'', + b'', + ]) + + + def test_unknownTransferEncoding(self): + """ + A request whose C{transfer-encoding} header includes a value + other than C{chunked} or C{identity} fails with a 400 response + without calling L{Request.process}. + """ + self.assertRequestRejected([ + b'GET /a HTTP/1.1', + b'Transfer-Encoding: unknown', + b'Host: host.invalid', + b'', + b'', + ]) + + + def test_transferEncodingIdentity(self): + """ + A request with a valid C{content-length} and a + C{transfer-encoding} whose value is C{identity} succeeds. + """ + body = [] + + class SuccessfulRequest(http.Request): + processed = False + def process(self): + body.append(self.content.read()) + self.setHeader(b'content-length', b'0') + self.finish() + + request = b'''\ +GET / HTTP/1.1 +Host: host.invalid +Content-Length: 2 +Transfer-Encoding: identity + +ok +''' + channel = self.runRequest(request, SuccessfulRequest, False) + self.assertEqual(body, [b'ok']) + self.assertEqual( + channel.transport.value(), + b'HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n', + ) + + class QueryArgumentsTests(unittest.TestCase): def testParseqs(self):
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
20- github.com/advisories/GHSA-h96w-mmrf-2h6vghsaADVISORY
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/6ISMZFZBWW4EV6ETJGXAYIXN3AT7GBPL/mitrevendor-advisoryx_refsource_FEDORA
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/YW3NIL7VXSGJND2Q4BSXM3CFTAFU6T7D/mitrevendor-advisoryx_refsource_FEDORA
- nvd.nist.gov/vuln/detail/CVE-2020-10108ghsaADVISORY
- security.gentoo.org/glsa/202007-24ghsavendor-advisoryx_refsource_GENTOOWEB
- usn.ubuntu.com/4308-1/mitrevendor-advisoryx_refsource_UBUNTU
- usn.ubuntu.com/4308-2/mitrevendor-advisoryx_refsource_UBUNTU
- github.com/pypa/advisory-database/tree/main/vulns/twisted/PYSEC-2020-259.yamlghsaWEB
- github.com/twisted/twisted/blob/6ff2c40e42416c83203422ff70dfc49d2681c8e2/NEWS.rstghsaWEB
- github.com/twisted/twisted/commit/4a7d22e490bb8ff836892cc99a1f54b85ccb0281ghsaWEB
- know.bishopfox.com/advisoriesghsax_refsource_MISCWEB
- know.bishopfox.com/advisories/twisted-version-19.10.0ghsax_refsource_MISCWEB
- lists.debian.org/debian-lts-announce/2022/02/msg00021.htmlghsamailing-listx_refsource_MLISTWEB
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/6ISMZFZBWW4EV6ETJGXAYIXN3AT7GBPLghsaWEB
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/YW3NIL7VXSGJND2Q4BSXM3CFTAFU6T7DghsaWEB
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/6ISMZFZBWW4EV6ETJGXAYIXN3AT7GBPLghsaWEB
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/YW3NIL7VXSGJND2Q4BSXM3CFTAFU6T7DghsaWEB
- usn.ubuntu.com/4308-1ghsaWEB
- usn.ubuntu.com/4308-2ghsaWEB
- www.oracle.com/security-alerts/cpuoct2020.htmlghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.