summaryrefslogtreecommitdiff
path: root/user/py3-twisted
diff options
context:
space:
mode:
authorMax Rees <maxcrees@me.com>2020-03-18 15:38:06 -0500
committerMax Rees <maxcrees@me.com>2020-03-19 22:21:25 -0500
commit81e8d43ca2221db97afb8e7000381918a8f6ee3f (patch)
tree94b4ce1036955fc869f0827aefd775f71d64d1a3 /user/py3-twisted
parent9c855993eb92e8d569e35698fb4f632e2e4de52c (diff)
downloadpackages-81e8d43ca2221db97afb8e7000381918a8f6ee3f.tar.gz
packages-81e8d43ca2221db97afb8e7000381918a8f6ee3f.tar.bz2
packages-81e8d43ca2221db97afb8e7000381918a8f6ee3f.tar.xz
packages-81e8d43ca2221db97afb8e7000381918a8f6ee3f.zip
user/py3-twisted: bump to 19.10.0, patch CVE-2020-10108 and CVE-2020-10109
Diffstat (limited to 'user/py3-twisted')
-rw-r--r--user/py3-twisted/APKBUILD19
-rw-r--r--user/py3-twisted/CVE-2020-10108-and-2020-10109.patch260
2 files changed, 275 insertions, 4 deletions
diff --git a/user/py3-twisted/APKBUILD b/user/py3-twisted/APKBUILD
index 37f2b0976..bc6bc3176 100644
--- a/user/py3-twisted/APKBUILD
+++ b/user/py3-twisted/APKBUILD
@@ -4,18 +4,28 @@ pkgname=py3-twisted
_pkgname=Twisted
_p="${_pkgname#?}"
_p="${_pkgname%"$_p"}"
-pkgver=19.7.0
+pkgver=19.10.0
pkgrel=0
pkgdesc="Asynchronous networking framework written in Python"
url="https://twistedmatrix.com/"
arch="all"
+# FIXME: constantly, automat, hyperlink, and PyHamcrest
+# also need to be packaged and added to depends=
+options="net"
license="MIT"
-depends="python3 py3-incremental"
+depends="python3 py3-attrs py3-incremental py3-zope-interface"
makedepends="python3-dev"
subpackages=""
-source="https://files.pythonhosted.org/packages/source/$_p/$_pkgname/$_pkgname-$pkgver.tar.bz2"
+source="https://files.pythonhosted.org/packages/source/$_p/$_pkgname/$_pkgname-$pkgver.tar.bz2
+ CVE-2020-10108-and-2020-10109.patch
+ "
builddir="$srcdir/Twisted-$pkgver"
+# secfixes: twisted
+# 19.10.0-r0:
+# - CVE-2020-10108
+# - CVE-2020-10109
+
build() {
python3 setup.py build
}
@@ -28,4 +38,5 @@ package() {
python3 setup.py install --prefix=/usr --root="$pkgdir"
}
-sha512sums="46588008f0be63f9ec8cfb88bb81f4268e59e8dead8256c36144b521eb3e58726f4d8c9016b7157365b26929e39a3fa6ff2cc2a9f83e8cfa7f1acc43d31297c4 Twisted-19.7.0.tar.bz2"
+sha512sums="de8d7fd0b2081cebeff68b060c8469377011648bc563a94a993d3530fb007ed42c3a54925c9a10c465ee7a3065cc9108ace12d10d358223fab13494becb9ac4b Twisted-19.10.0.tar.bz2
+dcc22af0a72bce5cca3c9f5ee739b17f14275ac8e776c658743746239c793ad9585fb0333203945912fd4994bd001b7244905562c4389304a707a2a4430faa40 CVE-2020-10108-and-2020-10109.patch"
diff --git a/user/py3-twisted/CVE-2020-10108-and-2020-10109.patch b/user/py3-twisted/CVE-2020-10108-and-2020-10109.patch
new file mode 100644
index 000000000..9a58f5584
--- /dev/null
+++ b/user/py3-twisted/CVE-2020-10108-and-2020-10109.patch
@@ -0,0 +1,260 @@
+From 4a7d22e490bb8ff836892cc99a1f54b85ccb0281 Mon Sep 17 00:00:00 2001
+From: Mark Williams <mrw@enotuniq.org>
+Date: Sun, 16 Feb 2020 19:00:10 -0800
+Subject: [PATCH] Fix several request smuggling attacks.
+
+1. Requests with multiple Content-Length headers were allowed (thanks
+to Jake Miller from Bishop Fox and ZeddYu Lu) and now fail with a 400;
+
+2. 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;
+
+3. Requests whose Transfer-Encoding header had a value other than
+"chunked" and "identity" (thanks to ZeddYu Lu) were allowed and now fail
+with a 400.
+---
+ src/twisted/web/http.py | 64 +++++++---
+ src/twisted/web/newsfragments/9770.bugfix | 1 +
+ src/twisted/web/test/test_http.py | 137 ++++++++++++++++++++++
+ 3 files changed, 187 insertions(+), 15 deletions(-)
+ create mode 100644 src/twisted/web/newsfragments/9770.bugfix
+
+diff --git a/src/twisted/web/http.py b/src/twisted/web/http.py
+index f0fb05b4d69..06d830fe30f 100644
+--- a/src/twisted/web/http.py
++++ b/src/twisted/web/http.py
+@@ -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:
+diff --git a/src/twisted/web/newsfragments/9770.bugfix b/src/twisted/web/newsfragments/9770.bugfix
+new file mode 100644
+index 00000000000..4f1be97de8a
+--- /dev/null
++++ b/src/twisted/web/newsfragments/9770.bugfix
+@@ -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
+diff --git a/src/twisted/web/test/test_http.py b/src/twisted/web/test/test_http.py
+index 0a0db09b750..578cb500cda 100644
+--- a/src/twisted/web/test/test_http.py
++++ b/src/twisted/web/test/test_http.py
+@@ -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):