Version in base suite: 18.9.0-3 Base version: twisted_18.9.0-3 Target version: twisted_18.9.0-3+deb10u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/t/twisted/twisted_18.9.0-3.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/t/twisted/twisted_18.9.0-3+deb10u1.dsc changelog | 61 patches/CVE-2019-12387.patch | 962 ++++++++++ patches/CVE-2019-12855-01.patch | 176 + patches/CVE-2019-12855-02.patch | 25 patches/CVE-2019-12855-03.patch | 80 patches/CVE-2019-12855-04.patch | 26 patches/CVE-2019-12855-05.patch | 180 + patches/CVE-2019-12855-06.patch | 179 + patches/CVE-2019-12855-07.patch | 31 patches/CVE-2019-12855-09.patch | 23 patches/CVE-2019-12855-10.patch | 105 + patches/CVE-2019-12855-11.patch | 46 patches/CVE-2019-12855-12.patch | 34 patches/CVE-2019-12855-13.patch | 211 ++ patches/CVE-2019-12855-14.patch | 40 patches/CVE-2019-12855-15.patch | 59 patches/CVE-2019-12855-17.patch | 41 patches/CVE-2019-951x.patch | 627 ++++++ patches/CVE-2020-1010x-pre1.patch | 135 + patches/CVE-2020-1010x.patch | 248 ++ patches/CVE-2022-21712-1.patch | 262 ++ patches/CVE-2022-21712-10.patch | 29 patches/CVE-2022-21712-2.patch | 21 patches/CVE-2022-21712-3.patch | 235 ++ patches/CVE-2022-21712-4.patch | 15 patches/CVE-2022-21712-5.patch | 218 ++ patches/CVE-2022-21712-6.patch | 20 patches/CVE-2022-21712-7.patch | 142 + patches/CVE-2022-21712-8.patch | 124 + patches/CVE-2022-21712-9.patch | 19 patches/CVE-2022-21716-1.patch | 66 patches/CVE-2022-21716-2.patch | 49 patches/CVE-2022-21716-3.patch | 20 patches/CVE-2022-24801-1.patch | 129 + patches/CVE-2022-24801-2.patch | 21 patches/CVE-2022-24801-3.patch | 21 patches/CVE-2022-24801-4.patch | 21 patches/CVE-2022-24801-5.patch | 69 patches/CVE-2022-24801-6.patch | 112 + patches/CVE-2022-24801-7.patch | 22 patches/CVE-2022-24801-8.patch | 100 + patches/CVE-2022-24801-9.patch | 38 patches/Tests-remove-spurious-test-for-illegal-whitespace-in-xmlns.patch | 37 patches/series | 42 44 files changed, 5121 insertions(+) diff -Nru twisted-18.9.0/debian/changelog twisted-18.9.0/debian/changelog --- twisted-18.9.0/debian/changelog 2018-12-07 10:23:30.000000000 +0000 +++ twisted-18.9.0/debian/changelog 2022-05-05 14:01:06.000000000 +0000 @@ -1,3 +1,64 @@ +twisted (18.9.0-3+deb10u1) buster; urgency=medium + + * Team upload. + * SECURITY UPDATE: incorrect URI and HTTP method validation + - debian/patches/CVE-2019-12387.patch: prevent CRLF injections in + src/twisted/web/_newclient.py, src/twisted/web/client.py, + src/twisted/web/test/injectionhelpers.py, + src/twisted/web/test/test_agent.py, + src/twisted/web/test/test_webclient.py. + - CVE-2019-12387 + - Thanks Marc Deslauriers at Canonical for backporting the patches. + * SECURITY UPDATE: incorrect cert validation in XMPP support + - debian/patches/CVE-2019-12855-*.patch: upstream patches to implement + certificate checking. + - CVE-2019-12855 + - Thanks Marc Deslauriers at Canonical for backporting the patches. + * SECURITY UPDATE: HTTP/2 denial of service issues + - debian/patches/CVE-2019-951x.patch: buffer outbound control frames + and timeout invalid clients in src/twisted/web/_http2.py, + src/twisted/web/error.py, src/twisted/web/http.py, + src/twisted/web/test/test_http.py, + src/twisted/web/test/test_http2.py. + - CVE-2019-9511 + - CVE-2019-9514 + - CVE-2019-9515 + - Thanks Marc Deslauriers at Canonical for backporting the patches. + * SECURITY UPDATE: request smuggling attacks + - debian/patches/CVE-2020-1010x-pre1.patch: refactor to reduce + duplication in src/twisted/web/test/test_http.py. + - debian/patches/CVE-2020-1010x.patch: fix several request smuggling + attacks in src/twisted/web/http.py, + src/twisted/web/test/test_http.py. + - CVE-2020-10108 + - CVE-2020-10109 + - Thanks Marc Deslauriers at Canonical for backporting the patches. + * SECURITY UPDATE: Information disclosure results in leaking of HTTP cookie + and authorization headers when following cross origin redirects + - debian/patches/CVE-2022-21712-*.patch: Ensure sensitive HTTP headers are + removed when forming requests, in src/twisted/web/client.py, + src/twisted/web/test/test_agent.py and src/twisted/web/iweb.py. + - CVE-2022-21712 + - Thanks Ray Veldkamp at Canonical for backporting the patches. + * SECURITY UPDATE: Parsing of SSH version identifier field during an SSH + handshake can result in a denial of service when excessively large packets + are received + - debian/patches/CVE-2022-21716-*.patch: Ensure that length of received + handshake buffer is checked, prior to processing version string in + src/twisted/conch/ssh/transport.py and + src/twisted/conch/test/test_transport.py + - CVE-2022-21716 + - Thanks Ray Veldkamp at Canonical for backporting the patches. + * CVE-2022-24801: Correct several defects in HTTP request parsing that could + permit HTTP request smuggling: 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. + - debian/patches/CVE-2022-24801-*.patch + * Patch: remove spurious test for illegal whitespace in xmlns, to allow + tests to pass, again. + + -- Stefano Rivera Thu, 05 May 2022 10:01:06 -0400 + twisted (18.9.0-3) unstable; urgency=medium * Don't fix package substvars in binary-arch:, they did not depend on hamcrest diff -Nru twisted-18.9.0/debian/patches/CVE-2019-12387.patch twisted-18.9.0/debian/patches/CVE-2019-12387.patch --- twisted-18.9.0/debian/patches/CVE-2019-12387.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2019-12387.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,962 @@ +Backport of: + +From 6c61fc4503ae39ab8ecee52d10f10ee2c371d7e2 Mon Sep 17 00:00:00 2001 +From: Mark Williams +Date: Wed, 5 Jun 2019 00:03:37 -0700 +Subject: [PATCH] Prevent CRLF injections described in CVE-2019-12387 + +Author: markrwilliams + +Reviewers: glyph + +Fixes: ticket:9647 + +Twisted's HTTP client APIs were vulnerable to maliciously constructed +HTTP methods, hosts, and/or paths, URI components such as paths and +query parameters. These vulnerabilities were beyond the header name +and value injection vulnerabilities addressed in: + +https://twistedmatrix.com/trac/ticket/9420 +https://github.com/twisted/twisted/pull/999/ + +The following client APIs will raise a ValueError if given a method, +host, or URI that includes newlines or other disallowed characters: + +- twisted.web.client.Agent.request +- twisted.web.client.ProxyAgent.request +- twisted.web.client.Request.__init__ +- twisted.web.client.Request.writeTo + +ProxyAgent is patched separately from Agent because unlike other +agents (e.g. CookieAgent) it is not implemented as an Agent wrapper. + +Request.__init__ checks its method and URI so that errors occur closer +to their originating input. Request.method and Request.uri are both +public APIs, however, so Request.writeTo (via Request._writeHeaders) +also checks the validity of both before writing anything to the wire. + +Additionally, the following deprecated client APIs have also been +patched: + +- twisted.web.client.HTTPPageGetter.__init__ +- twisted.web.client.HTTPPageDownloader.__init__ +- twisted.web.client.HTTPClientFactory.__init__ +- twisted.web.client.HTTPClientFactory.setURL +- twisted.web.client.HTTPDownloader.__init__ +- twisted.web.client.HTTPDownloader.setURL +- twisted.web.client.getPage +- twisted.web.client.downloadPage + +These have been patched prior to their removal so that they won't be +vulnerable in the last Twisted release that includes them. They +represent a best effort, because testing every combination of these +public APIs would require more code than deprecated APIs warrant. + +In all cases URI components, including hostnames, are restricted to +the characters allowed in path components. This mirrors the CPython +patch (for bpo-30458) that addresses equivalent vulnerabilities: + +https://github.com/python/cpython/commit/bb8071a4cae5ab3fe321481dd3d73662ffb26052 + +HTTP methods, however, are checked against the set of characters +described in RFC-7230. +--- + src/twisted/web/_newclient.py | 85 +++++- + src/twisted/web/client.py | 22 +- + src/twisted/web/newsfragments/9647.bugfix | 1 + + src/twisted/web/test/injectionhelpers.py | 168 ++++++++++++ + src/twisted/web/test/test_agent.py | 147 +++++++++- + src/twisted/web/test/test_webclient.py | 313 +++++++++++++++++++++- + 6 files changed, 725 insertions(+), 11 deletions(-) + create mode 100644 src/twisted/web/newsfragments/9647.bugfix + create mode 100644 src/twisted/web/test/injectionhelpers.py + +--- a/src/twisted/web/_newclient.py ++++ b/src/twisted/web/_newclient.py +@@ -29,6 +29,8 @@ Various other classes in this module sup + from __future__ import division, absolute_import + __metaclass__ = type + ++import re ++ + from zope.interface import implementer + + from twisted.python.compat import networkString +@@ -579,6 +581,74 @@ class HTTPClientParser(HTTPParser): + + + ++_VALID_METHOD = re.compile( ++ br"\A[%s]+\Z" % ( ++ bytes().join( ++ ( ++ b"!", b"#", b"$", b"%", b"&", b"'", b"*", ++ b"+", b"-", b".", b"^", b"_", b"`", b"|", b"~", ++ b"\x30-\x39", ++ b"\x41-\x5a", ++ b"\x61-\x7A", ++ ), ++ ), ++ ), ++) ++ ++ ++ ++def _ensureValidMethod(method): ++ """ ++ An HTTP method is an HTTP token, which consists of any visible ++ ASCII character that is not a delimiter (i.e. one of ++ C{"(),/:;<=>?@[\\]{}}.) ++ ++ @param method: the method to check ++ @type method: L{bytes} ++ ++ @return: the method if it is valid ++ @rtype: L{bytes} ++ ++ @raise ValueError: if the method is not valid ++ ++ @see: U{https://tools.ietf.org/html/rfc7230#section-3.1.1}, ++ U{https://tools.ietf.org/html/rfc7230#section-3.2.6}, ++ U{https://tools.ietf.org/html/rfc5234#appendix-B.1} ++ """ ++ if _VALID_METHOD.match(method): ++ return method ++ raise ValueError("Invalid method {!r}".format(method)) ++ ++ ++ ++_VALID_URI = re.compile(br'\A[\x21-\x7e]+\Z') ++ ++ ++ ++def _ensureValidURI(uri): ++ """ ++ A valid URI cannot contain control characters (i.e., characters ++ between 0-32, inclusive and 127) or non-ASCII characters (i.e., ++ characters with values between 128-255, inclusive). ++ ++ @param uri: the URI to check ++ @type uri: L{bytes} ++ ++ @return: the URI if it is valid ++ @rtype: L{bytes} ++ ++ @raise ValueError: if the URI is not valid ++ ++ @see: U{https://tools.ietf.org/html/rfc3986#section-3.3}, ++ U{https://tools.ietf.org/html/rfc3986#appendix-A}, ++ U{https://tools.ietf.org/html/rfc5234#appendix-B.1} ++ """ ++ if _VALID_URI.match(uri): ++ return uri ++ raise ValueError("Invalid URI {!r}".format(uri)) ++ ++ ++ + @implementer(IClientRequest) + class Request: + """ +@@ -618,8 +688,8 @@ class Request: + connection, defaults to C{False}. + @type persistent: L{bool} + """ +- self.method = method +- self.uri = uri ++ self.method = _ensureValidMethod(method) ++ self.uri = _ensureValidURI(uri) + self.headers = headers + self.bodyProducer = bodyProducer + self.persistent = persistent +@@ -664,8 +734,15 @@ class Request: + # method would probably be good. It would be nice if this method + # weren't limited to issuing HTTP/1.1 requests. + requestLines = [] +- requestLines.append(b' '.join([self.method, self.uri, +- b'HTTP/1.1\r\n'])) ++ requestLines.append( ++ b' '.join( ++ [ ++ _ensureValidMethod(self.method), ++ _ensureValidURI(self.uri), ++ b'HTTP/1.1\r\n', ++ ] ++ ), ++ ) + if not self.persistent: + requestLines.append(b'Connection: close\r\n') + if TEorCL is not None: +--- a/src/twisted/web/client.py ++++ b/src/twisted/web/client.py +@@ -46,6 +46,9 @@ from twisted.web.iweb import UNKNOWN_LEN + from twisted.web.http_headers import Headers + from twisted.logger import Logger + ++from twisted.web._newclient import _ensureValidURI, _ensureValidMethod ++ ++ + + class PartialDownloadError(error.Error): + """ +@@ -77,11 +80,13 @@ class HTTPPageGetter(http.HTTPClient): + + _completelyDone = True + +- _specialHeaders = set((b'host', b'user-agent', b'cookie', b'content-length')) ++ _specialHeaders = set( ++ (b'host', b'user-agent', b'cookie', b'content-length'), ++ ) + + def connectionMade(self): +- method = getattr(self.factory, 'method', b'GET') +- self.sendCommand(method, self.factory.path) ++ method = _ensureValidMethod(getattr(self.factory, 'method', b'GET')) ++ self.sendCommand(method, _ensureValidURI(self.factory.path)) + if self.factory.scheme == b'http' and self.factory.port != 80: + host = self.factory.host + b':' + intToBytes(self.factory.port) + elif self.factory.scheme == b'https' and self.factory.port != 443: +@@ -361,7 +366,7 @@ class HTTPClientFactory(protocol.ClientF + # just in case a broken http/1.1 decides to keep connection alive + self.headers.setdefault(b"connection", b"close") + self.postdata = postdata +- self.method = method ++ self.method = _ensureValidMethod(method) + + self.setURL(url) + +@@ -388,6 +393,7 @@ class HTTPClientFactory(protocol.ClientF + return "<%s: %s>" % (self.__class__.__name__, self.url) + + def setURL(self, url): ++ _ensureValidURI(url.strip()) + self.url = url + uri = URI.fromBytes(url) + if uri.scheme and uri.host: +@@ -732,7 +738,7 @@ def _makeGetterFactory(url, factoryFacto + + @return: The factory created by C{factoryFactory} + """ +- uri = URI.fromBytes(url) ++ uri = URI.fromBytes(_ensureValidURI(url.strip())) + factory = factoryFactory(url, *args, **kwargs) + if uri.scheme == b'https': + from twisted.internet import ssl +@@ -1422,6 +1428,9 @@ class _AgentBase(object): + Issue a new request, given the endpoint and the path sent as part of + the request. + """ ++ ++ method = _ensureValidMethod(method) ++ + # Create minimal headers, if necessary: + if headers is None: + headers = Headers() +@@ -1646,6 +1655,7 @@ class Agent(_AgentBase): + + @see: L{twisted.web.iweb.IAgent.request} + """ ++ uri = _ensureValidURI(uri.strip()) + parsedURI = URI.fromBytes(uri) + try: + endpoint = self._getEndpoint(parsedURI) +@@ -1679,6 +1689,8 @@ class ProxyAgent(_AgentBase): + """ + Issue a new request via the configured proxy. + """ ++ uri = _ensureValidURI(uri.strip()) ++ + # Cache *all* connections under the same key, since we are only + # connecting to a single destination, the proxy: + key = ("http-proxy", self._proxyEndpoint) +--- /dev/null ++++ b/src/twisted/web/test/injectionhelpers.py +@@ -0,0 +1,168 @@ ++""" ++Helpers for URI and method injection tests. ++ ++@see: U{CVE-2019-12387} ++""" ++ ++import string ++ ++ ++UNPRINTABLE_ASCII = ( ++ frozenset(range(0, 128)) - ++ frozenset(bytearray(string.printable, 'ascii')) ++) ++ ++NONASCII = frozenset(range(128, 256)) ++ ++ ++ ++class MethodInjectionTestsMixin(object): ++ """ ++ A mixin that runs HTTP method injection tests. Define ++ L{MethodInjectionTestsMixin.attemptRequestWithMaliciousMethod} in ++ a L{twisted.trial.unittest.SynchronousTestCase} subclass to test ++ how HTTP client code behaves when presented with malicious HTTP ++ methods. ++ ++ @see: U{CVE-2019-12387} ++ """ ++ ++ def attemptRequestWithMaliciousMethod(self, method): ++ """ ++ Attempt to send a request with the given method. This should ++ synchronously raise a L{ValueError} if either is invalid. ++ ++ @param method: the method (e.g. C{GET\x00}) ++ ++ @param uri: the URI ++ ++ @type method: ++ """ ++ raise NotImplementedError() ++ ++ ++ def test_methodWithCLRFRejected(self): ++ """ ++ Issuing a request with a method that contains a carriage ++ return and line feed fails with a L{ValueError}. ++ """ ++ with self.assertRaises(ValueError) as cm: ++ method = b"GET\r\nX-Injected-Header: value" ++ self.attemptRequestWithMaliciousMethod(method) ++ self.assertRegex(str(cm.exception), "^Invalid method") ++ ++ ++ def test_methodWithUnprintableASCIIRejected(self): ++ """ ++ Issuing a request with a method that contains unprintable ++ ASCII characters fails with a L{ValueError}. ++ """ ++ for c in UNPRINTABLE_ASCII: ++ method = b"GET%s" % (bytearray([c]),) ++ with self.assertRaises(ValueError) as cm: ++ self.attemptRequestWithMaliciousMethod(method) ++ self.assertRegex(str(cm.exception), "^Invalid method") ++ ++ ++ def test_methodWithNonASCIIRejected(self): ++ """ ++ Issuing a request with a method that contains non-ASCII ++ characters fails with a L{ValueError}. ++ """ ++ for c in NONASCII: ++ method = b"GET%s" % (bytearray([c]),) ++ with self.assertRaises(ValueError) as cm: ++ self.attemptRequestWithMaliciousMethod(method) ++ self.assertRegex(str(cm.exception), "^Invalid method") ++ ++ ++ ++class URIInjectionTestsMixin(object): ++ """ ++ A mixin that runs HTTP URI injection tests. Define ++ L{MethodInjectionTestsMixin.attemptRequestWithMaliciousURI} in a ++ L{twisted.trial.unittest.SynchronousTestCase} subclass to test how ++ HTTP client code behaves when presented with malicious HTTP ++ URIs. ++ """ ++ ++ def attemptRequestWithMaliciousURI(self, method): ++ """ ++ Attempt to send a request with the given URI. This should ++ synchronously raise a L{ValueError} if either is invalid. ++ ++ @param uri: the URI. ++ ++ @type method: ++ """ ++ raise NotImplementedError() ++ ++ ++ def test_hostWithCRLFRejected(self): ++ """ ++ Issuing a request with a URI whose host contains a carriage ++ return and line feed fails with a L{ValueError}. ++ """ ++ with self.assertRaises(ValueError) as cm: ++ uri = b"http://twisted\r\n.invalid/path" ++ self.attemptRequestWithMaliciousURI(uri) ++ self.assertRegex(str(cm.exception), "^Invalid URI") ++ ++ ++ def test_hostWithWithUnprintableASCIIRejected(self): ++ """ ++ Issuing a request with a URI whose host contains unprintable ++ ASCII characters fails with a L{ValueError}. ++ """ ++ for c in UNPRINTABLE_ASCII: ++ uri = b"http://twisted%s.invalid/OK" % (bytearray([c]),) ++ with self.assertRaises(ValueError) as cm: ++ self.attemptRequestWithMaliciousURI(uri) ++ self.assertRegex(str(cm.exception), "^Invalid URI") ++ ++ ++ def test_hostWithNonASCIIRejected(self): ++ """ ++ Issuing a request with a URI whose host contains non-ASCII ++ characters fails with a L{ValueError}. ++ """ ++ for c in NONASCII: ++ uri = b"http://twisted%s.invalid/OK" % (bytearray([c]),) ++ with self.assertRaises(ValueError) as cm: ++ self.attemptRequestWithMaliciousURI(uri) ++ self.assertRegex(str(cm.exception), "^Invalid URI") ++ ++ ++ def test_pathWithCRLFRejected(self): ++ """ ++ Issuing a request with a URI whose path contains a carriage ++ return and line feed fails with a L{ValueError}. ++ """ ++ with self.assertRaises(ValueError) as cm: ++ uri = b"http://twisted.invalid/\r\npath" ++ self.attemptRequestWithMaliciousURI(uri) ++ self.assertRegex(str(cm.exception), "^Invalid URI") ++ ++ ++ def test_pathWithWithUnprintableASCIIRejected(self): ++ """ ++ Issuing a request with a URI whose path contains unprintable ++ ASCII characters fails with a L{ValueError}. ++ """ ++ for c in UNPRINTABLE_ASCII: ++ uri = b"http://twisted.invalid/OK%s" % (bytearray([c]),) ++ with self.assertRaises(ValueError) as cm: ++ self.attemptRequestWithMaliciousURI(uri) ++ self.assertRegex(str(cm.exception), "^Invalid URI") ++ ++ ++ def test_pathWithNonASCIIRejected(self): ++ """ ++ Issuing a request with a URI whose path contains non-ASCII ++ characters fails with a L{ValueError}. ++ """ ++ for c in NONASCII: ++ uri = b"http://twisted.invalid/OK%s" % (bytearray([c]),) ++ with self.assertRaises(ValueError) as cm: ++ self.attemptRequestWithMaliciousURI(uri) ++ self.assertRegex(str(cm.exception), "^Invalid URI") +--- a/src/twisted/web/test/test_agent.py ++++ b/src/twisted/web/test/test_agent.py +@@ -11,7 +11,7 @@ from io import BytesIO + + from zope.interface.verify import verifyObject + +-from twisted.trial.unittest import TestCase ++from twisted.trial.unittest import TestCase, SynchronousTestCase + from twisted.web import client, error, http_headers + from twisted.web._newclient import RequestNotSent, RequestTransmissionFailed + from twisted.web._newclient import ResponseNeverReceived, ResponseFailed +@@ -50,6 +50,10 @@ from twisted.internet.endpoints import H + from twisted.test.proto_helpers import AccumulatingProtocol + from twisted.test.iosim import IOPump, FakeTransport + from twisted.test.test_sslverify import certificatesForAuthorityAndServer ++from twisted.web.test.injectionhelpers import ( ++ MethodInjectionTestsMixin, ++ URIInjectionTestsMixin, ++) + from twisted.web.error import SchemeNotSupported + from twisted.logger import globalLogPublisher + +@@ -886,6 +890,7 @@ class AgentTests(TestCase, FakeReactorAn + """ + Tests for the new HTTP client API provided by L{Agent}. + """ ++ + def makeAgent(self): + """ + @return: a new L{twisted.web.client.Agent} instance +@@ -1307,6 +1312,48 @@ class AgentTests(TestCase, FakeReactorAn + + + ++class AgentMethodInjectionTests( ++ FakeReactorAndConnectMixin, ++ MethodInjectionTestsMixin, ++ SynchronousTestCase, ++): ++ """ ++ Test L{client.Agent} against HTTP method injections. ++ """ ++ ++ def attemptRequestWithMaliciousMethod(self, method): ++ """ ++ Attempt a request with the provided method. ++ ++ @param method: see L{MethodInjectionTestsMixin} ++ """ ++ agent = client.Agent(self.createReactor()) ++ uri = b"http://twisted.invalid" ++ agent.request(method, uri, client.Headers(), None) ++ ++ ++ ++class AgentURIInjectionTests( ++ FakeReactorAndConnectMixin, ++ URIInjectionTestsMixin, ++ SynchronousTestCase, ++): ++ """ ++ Test L{client.Agent} against URI injections. ++ """ ++ ++ def attemptRequestWithMaliciousURI(self, uri): ++ """ ++ Attempt a request with the provided method. ++ ++ @param uri: see L{URIInjectionTestsMixin} ++ """ ++ agent = client.Agent(self.createReactor()) ++ method = b"GET" ++ agent.request(method, uri, client.Headers(), None) ++ ++ ++ + class AgentHTTPSTests(TestCase, FakeReactorAndConnectMixin, + IntegrationTestingMixin): + """ +@@ -3105,3 +3152,101 @@ class ReadBodyTests(TestCase): + + warnings = self.flushWarnings() + self.assertEqual(len(warnings), 0) ++ ++ ++ ++class RequestMethodInjectionTests( ++ MethodInjectionTestsMixin, ++ SynchronousTestCase, ++): ++ """ ++ Test L{client.Request} against HTTP method injections. ++ """ ++ ++ def attemptRequestWithMaliciousMethod(self, method): ++ """ ++ Attempt a request with the provided method. ++ ++ @param method: see L{MethodInjectionTestsMixin} ++ """ ++ client.Request( ++ method=method, ++ uri=b"http://twisted.invalid", ++ headers=http_headers.Headers(), ++ bodyProducer=None, ++ ) ++ ++ ++ ++class RequestWriteToMethodInjectionTests( ++ MethodInjectionTestsMixin, ++ SynchronousTestCase, ++): ++ """ ++ Test L{client.Request.writeTo} against HTTP method injections. ++ """ ++ ++ def attemptRequestWithMaliciousMethod(self, method): ++ """ ++ Attempt a request with the provided method. ++ ++ @param method: see L{MethodInjectionTestsMixin} ++ """ ++ headers = http_headers.Headers({b"Host": [b"twisted.invalid"]}) ++ req = client.Request( ++ method=b"GET", ++ uri=b"http://twisted.invalid", ++ headers=headers, ++ bodyProducer=None, ++ ) ++ req.method = method ++ req.writeTo(StringTransport()) ++ ++ ++ ++class RequestURIInjectionTests( ++ URIInjectionTestsMixin, ++ SynchronousTestCase, ++): ++ """ ++ Test L{client.Request} against HTTP URI injections. ++ """ ++ ++ def attemptRequestWithMaliciousURI(self, uri): ++ """ ++ Attempt a request with the provided URI. ++ ++ @param method: see L{URIInjectionTestsMixin} ++ """ ++ client.Request( ++ method=b"GET", ++ uri=uri, ++ headers=http_headers.Headers(), ++ bodyProducer=None, ++ ) ++ ++ ++ ++class RequestWriteToURIInjectionTests( ++ URIInjectionTestsMixin, ++ SynchronousTestCase, ++): ++ """ ++ Test L{client.Request.writeTo} against HTTP method injections. ++ """ ++ ++ def attemptRequestWithMaliciousURI(self, uri): ++ """ ++ Attempt a request with the provided method. ++ ++ @param method: see L{URIInjectionTestsMixin} ++ """ ++ headers = http_headers.Headers({b"Host": [b"twisted.invalid"]}) ++ req = client.Request( ++ method=b"GET", ++ uri=b"http://twisted.invalid", ++ headers=headers, ++ bodyProducer=None, ++ ) ++ req.uri = uri ++ req.writeTo(StringTransport()) +--- a/src/twisted/web/test/test_webclient.py ++++ b/src/twisted/web/test/test_webclient.py +@@ -7,6 +7,7 @@ Tests for the old L{twisted.web.client} + + from __future__ import division, absolute_import + ++import io + import os + from errno import ENOSPC + +@@ -20,7 +21,8 @@ from twisted.trial import unittest, util + from twisted.web import server, client, error, resource + from twisted.web.static import Data + from twisted.web.util import Redirect +-from twisted.internet import reactor, defer, interfaces ++from twisted.internet import address, reactor, defer, interfaces ++from twisted.internet.protocol import ClientFactory + from twisted.python.filepath import FilePath + from twisted.protocols.policies import WrappingFactory + from twisted.test.proto_helpers import ( +@@ -35,6 +37,12 @@ from twisted import test + from twisted.logger import (globalLogPublisher, FilteringLogObserver, + LogLevelFilterPredicate, LogLevel, Logger) + ++from twisted.web.test.injectionhelpers import ( ++ MethodInjectionTestsMixin, ++ URIInjectionTestsMixin, ++) ++ ++ + + serverPEM = FilePath(test.__file__).sibling('server.pem') + serverPEMPath = serverPEM.asBytesMode().path +@@ -1519,3 +1527,306 @@ class DeprecationTests(unittest.TestCase + L{client.HTTPDownloader} is deprecated. + """ + self._testDeprecatedClass("HTTPDownloader") ++ ++ ++ ++class GetPageMethodInjectionTests( ++ MethodInjectionTestsMixin, ++ unittest.SynchronousTestCase, ++): ++ """ ++ Test L{client.getPage} against HTTP method injections. ++ """ ++ ++ def attemptRequestWithMaliciousMethod(self, method): ++ """ ++ Attempt a request with the provided method. ++ ++ @param method: see L{MethodInjectionTestsMixin} ++ """ ++ uri = b'http://twisted.invalid' ++ client.getPage(uri, method=method) ++ ++ ++ ++class GetPageURIInjectionTests( ++ URIInjectionTestsMixin, ++ unittest.SynchronousTestCase, ++): ++ """ ++ Test L{client.getPage} against URI injections. ++ """ ++ ++ def attemptRequestWithMaliciousURI(self, uri): ++ """ ++ Attempt a request with the provided URI. ++ ++ @param uri: see L{URIInjectionTestsMixin} ++ """ ++ client.getPage(uri) ++ ++ ++ ++class DownloadPageMethodInjectionTests( ++ MethodInjectionTestsMixin, ++ unittest.SynchronousTestCase, ++): ++ """ ++ Test L{client.getPage} against HTTP method injections. ++ """ ++ ++ def attemptRequestWithMaliciousMethod(self, method): ++ """ ++ Attempt a request with the provided method. ++ ++ @param method: see L{MethodInjectionTestsMixin} ++ """ ++ uri = b'http://twisted.invalid' ++ client.downloadPage(uri, file=io.BytesIO(), method=method) ++ ++ ++ ++class DownloadPageURIInjectionTests( ++ URIInjectionTestsMixin, ++ unittest.SynchronousTestCase, ++): ++ """ ++ Test L{client.downloadPage} against URI injections. ++ """ ++ ++ def attemptRequestWithMaliciousURI(self, uri): ++ """ ++ Attempt a request with the provided URI. ++ ++ @param uri: see L{URIInjectionTestsMixin} ++ """ ++ client.downloadPage(uri, file=io.BytesIO()) ++ ++ ++ ++def makeHTTPPageGetterFactory(protocolClass, method, host, path): ++ """ ++ Make a L{ClientFactory} that can be used with ++ L{client.HTTPPageGetter} and its subclasses. ++ ++ @param protocolClass: The protocol class ++ @type protocolClass: A subclass of L{client.HTTPPageGetter} ++ ++ @param method: the HTTP method ++ ++ @param host: the host ++ ++ @param path: The URI path ++ ++ @return: A L{ClientFactory}. ++ """ ++ factory = ClientFactory.forProtocol(protocolClass) ++ ++ factory.method = method ++ factory.host = host ++ factory.path = path ++ ++ factory.scheme = b"http" ++ factory.port = 0 ++ factory.headers = {} ++ factory.agent = b"User/Agent" ++ factory.cookies = {} ++ ++ return factory ++ ++ ++ ++class HTTPPageGetterMethodInjectionTests( ++ MethodInjectionTestsMixin, ++ unittest.SynchronousTestCase, ++): ++ """ ++ Test L{client.HTTPPageGetter} against HTTP method injections. ++ """ ++ protocolClass = client.HTTPPageGetter ++ ++ def attemptRequestWithMaliciousMethod(self, method): ++ """ ++ Attempt a request with the provided method. ++ ++ @param method: L{MethodInjectionTestsMixin} ++ """ ++ transport = StringTransport() ++ factory = makeHTTPPageGetterFactory( ++ self.protocolClass, ++ method=method, ++ host=b"twisted.invalid", ++ path=b"/", ++ ) ++ getter = factory.buildProtocol( ++ address.IPv4Address("TCP", "127.0.0.1", 0), ++ ) ++ getter.makeConnection(transport) ++ ++ ++ ++class HTTPPageGetterURIInjectionTests( ++ URIInjectionTestsMixin, ++ unittest.SynchronousTestCase, ++): ++ """ ++ Test L{client.HTTPPageGetter} against HTTP URI injections. ++ """ ++ protocolClass = client.HTTPPageGetter ++ ++ def attemptRequestWithMaliciousURI(self, uri): ++ """ ++ Attempt a request with the provided URI. ++ ++ @param uri: L{URIInjectionTestsMixin} ++ """ ++ transport = StringTransport() ++ # Setting the host and path to the same value is imprecise but ++ # doesn't require parsing an invalid URI. ++ factory = makeHTTPPageGetterFactory( ++ self.protocolClass, ++ method=b"GET", ++ host=uri, ++ path=uri, ++ ) ++ getter = factory.buildProtocol( ++ address.IPv4Address("TCP", "127.0.0.1", 0), ++ ) ++ getter.makeConnection(transport) ++ ++ ++ ++class HTTPPageDownloaderMethodInjectionTests( ++ HTTPPageGetterMethodInjectionTests ++): ++ ++ """ ++ Test L{client.HTTPPageDownloader} against HTTP method injections. ++ """ ++ protocolClass = client.HTTPPageDownloader ++ ++ ++ ++class HTTPPageDownloaderURIInjectionTests( ++ HTTPPageGetterURIInjectionTests ++): ++ """ ++ Test L{client.HTTPPageDownloader} against HTTP URI injections. ++ """ ++ protocolClass = client.HTTPPageDownloader ++ ++ ++ ++class HTTPClientFactoryMethodInjectionTests( ++ MethodInjectionTestsMixin, ++ unittest.SynchronousTestCase, ++): ++ """ ++ Tests L{client.HTTPClientFactory} against HTTP method injections. ++ """ ++ ++ def attemptRequestWithMaliciousMethod(self, method): ++ """ ++ Attempt a request with the provided method. ++ ++ @param method: L{MethodInjectionTestsMixin} ++ """ ++ client.HTTPClientFactory(b"https://twisted.invalid", method) ++ ++ ++ ++class HTTPClientFactoryURIInjectionTests( ++ URIInjectionTestsMixin, ++ unittest.SynchronousTestCase, ++): ++ """ ++ Tests L{client.HTTPClientFactory} against HTTP URI injections. ++ """ ++ ++ def attemptRequestWithMaliciousURI(self, uri): ++ """ ++ Attempt a request with the provided URI. ++ ++ @param uri: L{URIInjectionTestsMixin} ++ """ ++ client.HTTPClientFactory(uri) ++ ++ ++ ++class HTTPClientFactorySetURLURIInjectionTests( ++ URIInjectionTestsMixin, ++ unittest.SynchronousTestCase, ++): ++ """ ++ Tests L{client.HTTPClientFactory.setURL} against HTTP URI injections. ++ """ ++ ++ def attemptRequestWithMaliciousURI(self, uri): ++ """ ++ Attempt a request with the provided URI. ++ ++ @param uri: L{URIInjectionTestsMixin} ++ """ ++ client.HTTPClientFactory(b"https://twisted.invalid").setURL(uri) ++ ++ ++ ++class HTTPDownloaderMethodInjectionTests( ++ MethodInjectionTestsMixin, ++ unittest.SynchronousTestCase, ++): ++ """ ++ Tests L{client.HTTPDownloader} against HTTP method injections. ++ """ ++ ++ def attemptRequestWithMaliciousMethod(self, method): ++ """ ++ Attempt a request with the provided method. ++ ++ @param method: L{MethodInjectionTestsMixin} ++ """ ++ client.HTTPDownloader( ++ b"https://twisted.invalid", ++ io.BytesIO(), ++ method=method, ++ ) ++ ++ ++ ++class HTTPDownloaderURIInjectionTests( ++ URIInjectionTestsMixin, ++ unittest.SynchronousTestCase, ++): ++ """ ++ Tests L{client.HTTPDownloader} against HTTP URI injections. ++ """ ++ ++ def attemptRequestWithMaliciousURI(self, uri): ++ """ ++ Attempt a request with the provided URI. ++ ++ @param uri: L{URIInjectionTestsMixin} ++ """ ++ client.HTTPDownloader(uri, io.BytesIO()) ++ ++ ++ ++class HTTPDownloaderSetURLURIInjectionTests( ++ URIInjectionTestsMixin, ++ unittest.SynchronousTestCase, ++): ++ """ ++ Tests L{client.HTTPDownloader.setURL} against HTTP URI injections. ++ """ ++ ++ def attemptRequestWithMaliciousURI(self, uri): ++ """ ++ Attempt a request with the provided URI. ++ ++ @param uri: L{URIInjectionTestsMixin} ++ """ ++ downloader = client.HTTPDownloader( ++ b"https://twisted.invalid", ++ io.BytesIO(), ++ ) ++ downloader.setURL(uri) diff -Nru twisted-18.9.0/debian/patches/CVE-2019-12855-01.patch twisted-18.9.0/debian/patches/CVE-2019-12855-01.patch --- twisted-18.9.0/debian/patches/CVE-2019-12855-01.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2019-12855-01.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,176 @@ +From 488bdd0b80cd1084359e34b8d36ae536520b1f86 Mon Sep 17 00:00:00 2001 +From: Ralph Meijer +Date: Tue, 7 May 2019 12:26:14 -0400 +Subject: [PATCH 01/17] Use optionsForClientTLS to verify server certificate by + default + +--- + .../words/protocols/jabber/xmlstream.py | 2 +- + .../words/test/test_jabberxmlstream.py | 61 +++++++++++++------ + 2 files changed, 44 insertions(+), 19 deletions(-) + +diff --git a/src/twisted/words/protocols/jabber/xmlstream.py b/src/twisted/words/protocols/jabber/xmlstream.py +index c191e9ae219..70d9267b705 100644 +--- a/src/twisted/words/protocols/jabber/xmlstream.py ++++ b/src/twisted/words/protocols/jabber/xmlstream.py +@@ -414,7 +414,7 @@ def onProceed(self, obj): + """ + + self.xmlstream.removeObserver('/failure', self.onFailure) +- ctx = ssl.CertificateOptions() ++ ctx = ssl.optionsForClientTLS(self.xmlstream.otherEntity.host) + self.xmlstream.transport.startTLS(ctx) + self.xmlstream.reset() + self.xmlstream.sendHeader() +diff --git a/src/twisted/words/test/test_jabberxmlstream.py b/src/twisted/words/test/test_jabberxmlstream.py +index 302171d7297..ccccf87372c 100644 +--- a/src/twisted/words/test/test_jabberxmlstream.py ++++ b/src/twisted/words/test/test_jabberxmlstream.py +@@ -14,6 +14,7 @@ + from twisted.internet import defer, task + from twisted.internet.error import ConnectionLost + from twisted.internet.interfaces import IProtocolFactory ++from twisted.internet._sslverify import ClientTLSOptions + from twisted.python import failure + from twisted.python.compat import unicode + from twisted.test import proto_helpers +@@ -665,7 +666,7 @@ def setUp(self): + + self.savedSSL = xmlstream.ssl + +- self.authenticator = xmlstream.Authenticator() ++ self.authenticator = xmlstream.ConnectAuthenticator(u'example.com') + self.xmlstream = xmlstream.XmlStream(self.authenticator) + self.xmlstream.send = self.output.append + self.xmlstream.connectionMade() +@@ -679,9 +680,9 @@ def tearDown(self): + xmlstream.ssl = self.savedSSL + + +- def testWantedSupported(self): ++ def test_wantedSupported(self): + """ +- Test start when TLS is wanted and the SSL library available. ++ When TLS is wanted and SSL available, StartTLS is initiated. + """ + self.xmlstream.transport = proto_helpers.StringTransport() + self.xmlstream.transport.startTLS = lambda ctx: self.done.append('TLS') +@@ -690,7 +691,8 @@ def testWantedSupported(self): + + d = self.init.start() + d.addCallback(self.assertEqual, xmlstream.Reset) +- starttls = self.output[0] ++ self.assertEqual(2, len(self.output)) ++ starttls = self.output[1] + self.assertEqual('starttls', starttls.name) + self.assertEqual(NS_XMPP_TLS, starttls.uri) + self.xmlstream.dataReceived("" % NS_XMPP_TLS) +@@ -698,40 +700,63 @@ def testWantedSupported(self): + + return d + ++ ++ def test_certificateVerify(self): ++ """ ++ The server certificate will be verified. ++ """ ++ ++ def fakeStartTLS(contextFactory): ++ self.assertIsInstance(contextFactory, ClientTLSOptions) ++ self.assertEqual(contextFactory._hostname, u"example.com") ++ self.done.append('TLS') ++ ++ self.xmlstream.transport = proto_helpers.StringTransport() ++ self.xmlstream.transport.startTLS = fakeStartTLS ++ self.xmlstream.reset = lambda: self.done.append('reset') ++ self.xmlstream.sendHeader = lambda: self.done.append('header') ++ ++ d = self.init.start() ++ self.xmlstream.dataReceived("" % NS_XMPP_TLS) ++ self.assertEqual(['TLS', 'reset', 'header'], self.done) ++ return d ++ ++ + if not xmlstream.ssl: + testWantedSupported.skip = "SSL not available" ++ test_certificateVerify = "SSL not available" + + +- def testWantedNotSupportedNotRequired(self): ++ def test_wantedNotSupportedNotRequired(self): + """ +- Test start when TLS is wanted and the SSL library available. ++ No StartTLS is initiated when wanted, not required, SSL not available. + """ + xmlstream.ssl = None + + d = self.init.start() + d.addCallback(self.assertEqual, None) +- self.assertEqual([], self.output) ++ self.assertEqual(1, len(self.output)) + + return d + + +- def testWantedNotSupportedRequired(self): ++ def test_wantedNotSupportedRequired(self): + """ +- Test start when TLS is wanted and the SSL library available. ++ TLSNotSupported is raised when TLS is required but not available. + """ + xmlstream.ssl = None + self.init.required = True + + d = self.init.start() + self.assertFailure(d, xmlstream.TLSNotSupported) +- self.assertEqual([], self.output) ++ self.assertEqual(1, len(self.output)) + + return d + + +- def testNotWantedRequired(self): ++ def test_notWantedRequired(self): + """ +- Test start when TLS is not wanted, but required by the server. ++ TLSRequired is raised when TLS is not wanted, but required by server. + """ + tls = domish.Element(('urn:ietf:params:xml:ns:xmpp-tls', 'starttls')) + tls.addElement('required') +@@ -739,15 +764,15 @@ def testNotWantedRequired(self): + self.init.wanted = False + + d = self.init.start() +- self.assertEqual([], self.output) ++ self.assertEqual(1, len(self.output)) + self.assertFailure(d, xmlstream.TLSRequired) + + return d + + +- def testNotWantedNotRequired(self): ++ def test_notWantedNotRequired(self): + """ +- Test start when TLS is not wanted, but required by the server. ++ No StartTLS is initiated when not wanted and not required. + """ + tls = domish.Element(('urn:ietf:params:xml:ns:xmpp-tls', 'starttls')) + self.xmlstream.features = {(tls.uri, tls.name): tls} +@@ -755,13 +780,13 @@ def testNotWantedNotRequired(self): + + d = self.init.start() + d.addCallback(self.assertEqual, None) +- self.assertEqual([], self.output) ++ self.assertEqual(1, len(self.output)) + return d + + +- def testFailed(self): ++ def test_failed(self): + """ +- Test failed TLS negotiation. ++ TLSFailed is raised when the server responds with a failure. + """ + # Pretend that ssl is supported, it isn't actually used when the + # server starts out with a failure in response to our initial + diff -Nru twisted-18.9.0/debian/patches/CVE-2019-12855-02.patch twisted-18.9.0/debian/patches/CVE-2019-12855-02.patch --- twisted-18.9.0/debian/patches/CVE-2019-12855-02.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2019-12855-02.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,25 @@ +From 0ff32b1bf115acc90d223b9ce9820063cf89003d Mon Sep 17 00:00:00 2001 +From: Ralph Meijer +Date: Tue, 7 May 2019 15:54:33 -0400 +Subject: [PATCH 02/17] Fix client example to print disconnection reason + +--- + docs/words/examples/xmpp_client.py | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/docs/words/examples/xmpp_client.py b/docs/words/examples/xmpp_client.py +index cb80202c67f..4a3651009b4 100644 +--- a/docs/words/examples/xmpp_client.py ++++ b/docs/words/examples/xmpp_client.py +@@ -53,8 +53,9 @@ def connected(self, xs): + xs.rawDataOutFn = self.rawDataOut + + +- def disconnected(self, xs): ++ def disconnected(self, reason): + print('Disconnected.') ++ print(reason) + + self.finished.callback(None) + + diff -Nru twisted-18.9.0/debian/patches/CVE-2019-12855-03.patch twisted-18.9.0/debian/patches/CVE-2019-12855-03.patch --- twisted-18.9.0/debian/patches/CVE-2019-12855-03.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2019-12855-03.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,80 @@ +From 89954dfb18e613be583c74e22a3dd55d66e7d975 Mon Sep 17 00:00:00 2001 +From: Ralph Meijer +Date: Tue, 7 May 2019 18:23:49 -0400 +Subject: [PATCH 03/17] Allow for custom contextFactory to TLS initializer + +--- + .../words/protocols/jabber/xmlstream.py | 6 ++++- + .../words/test/test_jabberxmlstream.py | 24 +++++++++++++++++++ + 2 files changed, 29 insertions(+), 1 deletion(-) + +diff --git a/src/twisted/words/protocols/jabber/xmlstream.py b/src/twisted/words/protocols/jabber/xmlstream.py +index 70d9267b705..51a8466b16a 100644 +--- a/src/twisted/words/protocols/jabber/xmlstream.py ++++ b/src/twisted/words/protocols/jabber/xmlstream.py +@@ -406,6 +406,7 @@ class TLSInitiatingInitializer(BaseFeatureInitiatingInitializer): + + feature = (NS_XMPP_TLS, 'starttls') + wanted = True ++ contextFactory = None + _deferred = None + + def onProceed(self, obj): +@@ -414,7 +415,10 @@ def onProceed(self, obj): + """ + + self.xmlstream.removeObserver('/failure', self.onFailure) +- ctx = ssl.optionsForClientTLS(self.xmlstream.otherEntity.host) ++ if self.contextFactory: ++ ctx = self.contextFactory ++ else: ++ ctx = ssl.optionsForClientTLS(self.xmlstream.otherEntity.host) + self.xmlstream.transport.startTLS(ctx) + self.xmlstream.reset() + self.xmlstream.sendHeader() +diff --git a/src/twisted/words/test/test_jabberxmlstream.py b/src/twisted/words/test/test_jabberxmlstream.py +index ccccf87372c..863cad0f328 100644 +--- a/src/twisted/words/test/test_jabberxmlstream.py ++++ b/src/twisted/words/test/test_jabberxmlstream.py +@@ -14,6 +14,7 @@ + from twisted.internet import defer, task + from twisted.internet.error import ConnectionLost + from twisted.internet.interfaces import IProtocolFactory ++from twisted.internet.ssl import CertificateOptions + from twisted.internet._sslverify import ClientTLSOptions + from twisted.python import failure + from twisted.python.compat import unicode +@@ -722,9 +723,32 @@ def fakeStartTLS(contextFactory): + return d + + ++ def test_certificateVerifyContext(self): ++ """ ++ A custom contextFactory is passed through to startTLS. ++ """ ++ ctx = CertificateOptions() ++ self.init.contextFactory = ctx ++ ++ def fakeStartTLS(contextFactory): ++ self.assertIs(ctx, contextFactory) ++ self.done.append('TLS') ++ ++ self.xmlstream.transport = proto_helpers.StringTransport() ++ self.xmlstream.transport.startTLS = fakeStartTLS ++ self.xmlstream.reset = lambda: self.done.append('reset') ++ self.xmlstream.sendHeader = lambda: self.done.append('header') ++ ++ d = self.init.start() ++ self.xmlstream.dataReceived("" % NS_XMPP_TLS) ++ self.assertEqual(['TLS', 'reset', 'header'], self.done) ++ return d ++ ++ + if not xmlstream.ssl: + testWantedSupported.skip = "SSL not available" + test_certificateVerify = "SSL not available" ++ test_certificateVerifyContext = "SSL not available" + + + def test_wantedNotSupportedNotRequired(self): + diff -Nru twisted-18.9.0/debian/patches/CVE-2019-12855-04.patch twisted-18.9.0/debian/patches/CVE-2019-12855-04.patch --- twisted-18.9.0/debian/patches/CVE-2019-12855-04.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2019-12855-04.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,26 @@ +From 4759e27af0ffa2e61538d5e0a66c3e57e20d3f5b Mon Sep 17 00:00:00 2001 +From: Ralph Meijer +Date: Wed, 8 May 2019 13:19:17 -0400 +Subject: [PATCH 04/17] Add docstrings for new contextFactory attribute + +--- + src/twisted/words/protocols/jabber/xmlstream.py | 5 +++++ + 1 file changed, 5 insertions(+) + +diff --git a/src/twisted/words/protocols/jabber/xmlstream.py b/src/twisted/words/protocols/jabber/xmlstream.py +index 51a8466b16a..88ad21f76a6 100644 +--- a/src/twisted/words/protocols/jabber/xmlstream.py ++++ b/src/twisted/words/protocols/jabber/xmlstream.py +@@ -402,6 +402,11 @@ class TLSInitiatingInitializer(BaseFeatureInitiatingInitializer): + + @cvar wanted: indicates if TLS negotiation is wanted. + @type wanted: C{bool} ++ ++ @cvar contextFactory: An object which creates appropriately configured TLS ++ connections. This is passed to C{startTLS} on the transport and is ++ preferably created using L{twisted.internet.ssl.optionsForClientTLS}. ++ @type contextFactory: L{IOpenSSLClientConnectionCreator} + """ + + feature = (NS_XMPP_TLS, 'starttls') + diff -Nru twisted-18.9.0/debian/patches/CVE-2019-12855-05.patch twisted-18.9.0/debian/patches/CVE-2019-12855-05.patch --- twisted-18.9.0/debian/patches/CVE-2019-12855-05.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2019-12855-05.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,180 @@ +From fa18e8e65cf486ea9adc8e9a9a6df7e168098ce8 Mon Sep 17 00:00:00 2001 +From: Ralph Meijer +Date: Thu, 9 May 2019 11:11:14 -0400 +Subject: [PATCH 05/17] Clean up connecting authenticators + +This adds an option `required` argument to the inits of initializers +deriving from BaseFeatureInitiatingInitializer, to simplify setup. +Additionally it changes the requiredness of two initializers used by +XMPPAuthenticator: + +* Setup of TLS is now required by default. This ensures that if StartTLS +is not advertized by the server, initialization fails instead of +silently proceeding to authentication without encryption. +* Binding a resource is required by default, because without it servers +will not allow any further meaningful interaction. +--- + src/twisted/words/protocols/jabber/client.py | 28 +++++-------- + .../words/protocols/jabber/xmlstream.py | 9 +++-- + src/twisted/words/test/test_jabberclient.py | 39 ++++++++++++++++++- + .../words/test/test_jabberxmlstream.py | 9 +++++ + 4 files changed, 61 insertions(+), 24 deletions(-) + +diff --git a/src/twisted/words/protocols/jabber/client.py b/src/twisted/words/protocols/jabber/client.py +index ffe6c939d8a..566bc9ff177 100644 +--- a/src/twisted/words/protocols/jabber/client.py ++++ b/src/twisted/words/protocols/jabber/client.py +@@ -206,14 +206,10 @@ def associateWithStream(self, xs): + xs.version = (0, 0) + xmlstream.ConnectAuthenticator.associateWithStream(self, xs) + +- inits = [ (xmlstream.TLSInitiatingInitializer, False), +- (IQAuthInitializer, True), +- ] +- +- for initClass, required in inits: +- init = initClass(xs) +- init.required = required +- xs.initializers.append(init) ++ xs.initializers = [ ++ xmlstream.TLSInitiatingInitializer(xs, required=False), ++ IQAuthInitializer(xs), ++ ] + + # TODO: move registration into an Initializer? + +@@ -377,14 +373,10 @@ def associateWithStream(self, xs): + """ + xmlstream.ConnectAuthenticator.associateWithStream(self, xs) + +- xs.initializers = [CheckVersionInitializer(xs)] +- inits = [ (xmlstream.TLSInitiatingInitializer, False), +- (sasl.SASLInitiatingInitializer, True), +- (BindInitializer, False), +- (SessionInitializer, False), ++ xs.initializers = [ ++ CheckVersionInitializer(xs), ++ xmlstream.TLSInitiatingInitializer(xs, required=True), ++ sasl.SASLInitiatingInitializer(xs, required=True), ++ BindInitializer(xs, required=True), ++ SessionInitializer(xs, required=False), + ] +- +- for initClass, required in inits: +- init = initClass(xs) +- init.required = required +- xs.initializers.append(init) +diff --git a/src/twisted/words/protocols/jabber/xmlstream.py b/src/twisted/words/protocols/jabber/xmlstream.py +index 88ad21f76a6..f7512016c5a 100644 +--- a/src/twisted/words/protocols/jabber/xmlstream.py ++++ b/src/twisted/words/protocols/jabber/xmlstream.py +@@ -316,16 +316,17 @@ class BaseFeatureInitiatingInitializer(object): + + @cvar feature: tuple of (uri, name) of the stream feature root element. + @type feature: tuple of (C{str}, C{str}) ++ + @ivar required: whether the stream feature is required to be advertized + by the receiving entity. + @type required: C{bool} + """ + + feature = None +- required = False + +- def __init__(self, xs): ++ def __init__(self, xs, required=False): + self.xmlstream = xs ++ self.required = required + + + def initialize(self): +@@ -400,10 +401,10 @@ class TLSInitiatingInitializer(BaseFeatureInitiatingInitializer): + set the C{wanted} attribute to False instead of removing it from the list + of initializers, so a proper exception L{TLSRequired} can be raised. + +- @cvar wanted: indicates if TLS negotiation is wanted. ++ @ivar wanted: indicates if TLS negotiation is wanted. + @type wanted: C{bool} + +- @cvar contextFactory: An object which creates appropriately configured TLS ++ @ivar contextFactory: An object which creates appropriately configured TLS + connections. This is passed to C{startTLS} on the transport and is + preferably created using L{twisted.internet.ssl.optionsForClientTLS}. + @type contextFactory: L{IOpenSSLClientConnectionCreator} +diff --git a/src/twisted/words/test/test_jabberclient.py b/src/twisted/words/test/test_jabberclient.py +index d54f88651ad..19be60b34eb 100644 +--- a/src/twisted/words/test/test_jabberclient.py ++++ b/src/twisted/words/test/test_jabberclient.py +@@ -379,6 +379,41 @@ def onSession(iq): + + + ++class BasicAuthenticatorTests(unittest.TestCase): ++ """ ++ Test for both XMPPAuthenticator and XMPPClientFactory. ++ """ ++ def testBasic(self): ++ """ ++ Test basic operations. ++ ++ Setup a basicClientFactory, which sets up a BasicAuthenticator, and let ++ it produce a protocol instance. Then inspect the instance variables of ++ the authenticator and XML stream objects. ++ """ ++ self.client_jid = jid.JID('user@example.com/resource') ++ ++ # Get an XmlStream instance. Note that it gets initialized with the ++ # XMPPAuthenticator (that has its associateWithXmlStream called) that ++ # is in turn initialized with the arguments to the factory. ++ xs = client.basicClientFactory(self.client_jid, ++ 'secret').buildProtocol(None) ++ ++ # test authenticator's instance variables ++ self.assertEqual('example.com', xs.authenticator.otherHost) ++ self.assertEqual(self.client_jid, xs.authenticator.jid) ++ self.assertEqual('secret', xs.authenticator.password) ++ ++ # test list of initializers ++ tls, auth = xs.initializers ++ ++ self.assertIsInstance(tls, xmlstream.TLSInitiatingInitializer) ++ self.assertIsInstance(auth, client.IQAuthInitializer) ++ ++ self.assertFalse(tls.required) ++ ++ ++ + class XMPPAuthenticatorTests(unittest.TestCase): + """ + Test for both XMPPAuthenticator and XMPPClientFactory. +@@ -412,7 +447,7 @@ def testBasic(self): + self.assertIsInstance(bind, client.BindInitializer) + self.assertIsInstance(session, client.SessionInitializer) + +- self.assertFalse(tls.required) ++ self.assertTrue(tls.required) + self.assertTrue(sasl.required) +- self.assertFalse(bind.required) ++ self.assertTrue(bind.required) + self.assertFalse(session.required) +diff --git a/src/twisted/words/test/test_jabberxmlstream.py b/src/twisted/words/test/test_jabberxmlstream.py +index 863cad0f328..6df336deb20 100644 +--- a/src/twisted/words/test/test_jabberxmlstream.py ++++ b/src/twisted/words/test/test_jabberxmlstream.py +@@ -681,6 +681,15 @@ def tearDown(self): + xmlstream.ssl = self.savedSSL + + ++ def test_initRequired(self): ++ """ ++ Passing required sets the instance variable. ++ """ ++ self.init = xmlstream.TLSInitiatingInitializer(self.xmlstream, ++ required=True) ++ self.assertTrue(self.init.required) ++ ++ + def test_wantedSupported(self): + """ + When TLS is wanted and SSL available, StartTLS is initiated. + diff -Nru twisted-18.9.0/debian/patches/CVE-2019-12855-06.patch twisted-18.9.0/debian/patches/CVE-2019-12855-06.patch --- twisted-18.9.0/debian/patches/CVE-2019-12855-06.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2019-12855-06.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,179 @@ +From cadf08f3481b689929ad471a17ce29683dc0635d Mon Sep 17 00:00:00 2001 +From: Ralph Meijer +Date: Thu, 9 May 2019 12:05:21 -0400 +Subject: [PATCH 06/17] Provide a way to use custom certificate options for + XMPP clients + +This adds an optional `contextFactory` argument to `XMPPClientFactory` +that is passed on to `XMPPAuthenticator`, which in turn passes it to +`TLSInitiatingInitializer`. +--- + src/twisted/words/protocols/jabber/client.py | 25 ++++++++++--- + .../words/protocols/jabber/xmlstream.py | 9 +++++ + src/twisted/words/test/test_jabberclient.py | 35 ++++++++++++++++--- + 3 files changed, 61 insertions(+), 8 deletions(-) + +diff --git a/src/twisted/words/protocols/jabber/client.py b/src/twisted/words/protocols/jabber/client.py +index 566bc9ff177..4b310e34f38 100644 +--- a/src/twisted/words/protocols/jabber/client.py ++++ b/src/twisted/words/protocols/jabber/client.py +@@ -298,7 +298,7 @@ def start(self): + + + +-def XMPPClientFactory(jid, password): ++def XMPPClientFactory(jid, password, contextFactory=None): + """ + Client factory for XMPP 1.0 (only). + +@@ -310,12 +310,20 @@ def XMPPClientFactory(jid, password): + + @param jid: Jabber ID to connect with. + @type jid: L{jid.JID} ++ + @param password: password to authenticate with. + @type password: L{unicode} ++ ++ @param contextFactory: An object which creates appropriately configured TLS ++ connections. This is passed to C{startTLS} on the transport and is ++ preferably created using L{twisted.internet.ssl.optionsForClientTLS}. ++ See L{xmlstream.TLSInitiatingInitializer} for details. ++ @type contextFactory: L{IOpenSSLClientConnectionCreator} ++ + @return: XML stream factory. + @rtype: L{xmlstream.XmlStreamFactory} + """ +- a = XMPPAuthenticator(jid, password) ++ a = XMPPAuthenticator(jid, password, contextFactory=contextFactory) + return xmlstream.XmlStreamFactory(a) + + +@@ -350,16 +358,24 @@ class XMPPAuthenticator(xmlstream.ConnectAuthenticator): + resource binding step, and this is stored in this instance + variable. + @type jid: L{jid.JID} ++ + @ivar password: password to be used during SASL authentication. + @type password: L{unicode} ++ ++ @ivar contextFactory: An object which creates appropriately configured TLS ++ connections. This is passed to C{startTLS} on the transport and is ++ preferably created using L{twisted.internet.ssl.optionsForClientTLS}. ++ See L{xmlstream.TLSInitiatingInitializer} for details. ++ @type contextFactory: L{IOpenSSLClientConnectionCreator} + """ + + namespace = 'jabber:client' + +- def __init__(self, jid, password): ++ def __init__(self, jid, password, contextFactory=None): + xmlstream.ConnectAuthenticator.__init__(self, jid.host) + self.jid = jid + self.password = password ++ self.contextFactory = contextFactory + + + def associateWithStream(self, xs): +@@ -375,7 +391,8 @@ def associateWithStream(self, xs): + + xs.initializers = [ + CheckVersionInitializer(xs), +- xmlstream.TLSInitiatingInitializer(xs, required=True), ++ xmlstream.TLSInitiatingInitializer( ++ xs, required=True, contextFactory=self.contextFactory), + sasl.SASLInitiatingInitializer(xs, required=True), + BindInitializer(xs, required=True), + SessionInitializer(xs, required=False), +diff --git a/src/twisted/words/protocols/jabber/xmlstream.py b/src/twisted/words/protocols/jabber/xmlstream.py +index f7512016c5a..1ed79d47726 100644 +--- a/src/twisted/words/protocols/jabber/xmlstream.py ++++ b/src/twisted/words/protocols/jabber/xmlstream.py +@@ -407,6 +407,9 @@ class TLSInitiatingInitializer(BaseFeatureInitiatingInitializer): + @ivar contextFactory: An object which creates appropriately configured TLS + connections. This is passed to C{startTLS} on the transport and is + preferably created using L{twisted.internet.ssl.optionsForClientTLS}. ++ If C{None}, the default is to verify the server certificate against ++ the trust roots as provided by the platform. See ++ L{twisted.internet._sslverify.platformTrust}. + @type contextFactory: L{IOpenSSLClientConnectionCreator} + """ + +@@ -415,6 +418,12 @@ class TLSInitiatingInitializer(BaseFeatureInitiatingInitializer): + contextFactory = None + _deferred = None + ++ def __init__(self, xs, required=True, contextFactory=None): ++ super(TLSInitiatingInitializer, self).__init__( ++ xs, required=required) ++ self.contextFactory = contextFactory ++ ++ + def onProceed(self, obj): + """ + Proceed with TLS negotiation and reset the XML stream. +diff --git a/src/twisted/words/test/test_jabberclient.py b/src/twisted/words/test/test_jabberclient.py +index 19be60b34eb..2e39de72cee 100644 +--- a/src/twisted/words/test/test_jabberclient.py ++++ b/src/twisted/words/test/test_jabberclient.py +@@ -9,7 +9,7 @@ + + from hashlib import sha1 + +-from twisted.internet import defer ++from twisted.internet import defer, ssl + from twisted.python.compat import unicode + from twisted.trial import unittest + from twisted.words.protocols.jabber import client, error, jid, xmlstream +@@ -381,9 +381,10 @@ def onSession(iq): + + class BasicAuthenticatorTests(unittest.TestCase): + """ +- Test for both XMPPAuthenticator and XMPPClientFactory. ++ Test for both BasicAuthenticator and basicClientFactory. + """ +- def testBasic(self): ++ ++ def test_basic(self): + """ + Test basic operations. + +@@ -418,7 +419,8 @@ class XMPPAuthenticatorTests(unittest.TestCase): + """ + Test for both XMPPAuthenticator and XMPPClientFactory. + """ +- def testBasic(self): ++ ++ def test_basic(self): + """ + Test basic operations. + +@@ -451,3 +453,28 @@ def testBasic(self): + self.assertTrue(sasl.required) + self.assertTrue(bind.required) + self.assertFalse(session.required) ++ ++ ++ def test_tlsContextFactory(self): ++ """ ++ Test basic operations. ++ ++ Setup an XMPPClientFactory, which sets up an XMPPAuthenticator, and let ++ it produce a protocol instance. Then inspect the instance variables of ++ the authenticator and XML stream objects. ++ """ ++ self.client_jid = jid.JID('user@example.com/resource') ++ ++ # Get an XmlStream instance. Note that it gets initialized with the ++ # XMPPAuthenticator (that has its associateWithXmlStream called) that ++ # is in turn initialized with the arguments to the factory. ++ contextFactory = ssl.CertificateOptions() ++ factory = client.XMPPClientFactory(self.client_jid, 'secret', ++ contextFactory=contextFactory) ++ xs = factory.buildProtocol(None) ++ ++ # test list of initializers ++ version, tls, sasl, bind, session = xs.initializers ++ ++ self.assertIsInstance(tls, xmlstream.TLSInitiatingInitializer) ++ self.assertIs(contextFactory, tls.contextFactory) + diff -Nru twisted-18.9.0/debian/patches/CVE-2019-12855-07.patch twisted-18.9.0/debian/patches/CVE-2019-12855-07.patch --- twisted-18.9.0/debian/patches/CVE-2019-12855-07.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2019-12855-07.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,31 @@ +From 5ed194c0514a04500b3190b0ecbad0cce8b9b82d Mon Sep 17 00:00:00 2001 +From: Ralph Meijer +Date: Thu, 9 May 2019 12:12:32 -0400 +Subject: [PATCH 07/17] Adjust tests to TLSInitiatingInitializer being required + by default + +--- + src/twisted/words/test/test_jabberxmlstream.py | 2 ++ + 1 file changed, 2 insertions(+) + +diff --git a/src/twisted/words/test/test_jabberxmlstream.py b/src/twisted/words/test/test_jabberxmlstream.py +index 6df336deb20..2b8dcd9516e 100644 +--- a/src/twisted/words/test/test_jabberxmlstream.py ++++ b/src/twisted/words/test/test_jabberxmlstream.py +@@ -765,6 +765,7 @@ def test_wantedNotSupportedNotRequired(self): + No StartTLS is initiated when wanted, not required, SSL not available. + """ + xmlstream.ssl = None ++ self.init.required = False + + d = self.init.start() + d.addCallback(self.assertEqual, None) +@@ -810,6 +811,7 @@ def test_notWantedNotRequired(self): + tls = domish.Element(('urn:ietf:params:xml:ns:xmpp-tls', 'starttls')) + self.xmlstream.features = {(tls.uri, tls.name): tls} + self.init.wanted = False ++ self.init.required = False + + d = self.init.start() + d.addCallback(self.assertEqual, None) + diff -Nru twisted-18.9.0/debian/patches/CVE-2019-12855-09.patch twisted-18.9.0/debian/patches/CVE-2019-12855-09.patch --- twisted-18.9.0/debian/patches/CVE-2019-12855-09.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2019-12855-09.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,23 @@ +From 0a93949f91ea22cfc5453c326e36e927c8da1015 Mon Sep 17 00:00:00 2001 +From: Ralph Meijer +Date: Mon, 27 May 2019 13:53:31 +0200 +Subject: [PATCH 09/17] Fix skipping renamed test when SSL is not available + +--- + src/twisted/words/test/test_jabberxmlstream.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/twisted/words/test/test_jabberxmlstream.py b/src/twisted/words/test/test_jabberxmlstream.py +index 2b8dcd9516e..d9f4962ec0c 100644 +--- a/src/twisted/words/test/test_jabberxmlstream.py ++++ b/src/twisted/words/test/test_jabberxmlstream.py +@@ -755,7 +755,7 @@ def fakeStartTLS(contextFactory): + + + if not xmlstream.ssl: +- testWantedSupported.skip = "SSL not available" ++ test_wantedSupported.skip = "SSL not available" + test_certificateVerify = "SSL not available" + test_certificateVerifyContext = "SSL not available" + + diff -Nru twisted-18.9.0/debian/patches/CVE-2019-12855-10.patch twisted-18.9.0/debian/patches/CVE-2019-12855-10.patch --- twisted-18.9.0/debian/patches/CVE-2019-12855-10.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2019-12855-10.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,105 @@ +From 751ac6f754146e5b61ab65d2995be2a9534bd41d Mon Sep 17 00:00:00 2001 +From: Ralph Meijer +Date: Mon, 27 May 2019 14:48:26 +0200 +Subject: [PATCH 10/17] Skip TLS tests if OpenSSL is not available + +--- + src/twisted/words/test/test_jabberclient.py | 12 +++++++++- + .../words/test/test_jabberxmlstream.py | 22 ++++++++++++------- + 2 files changed, 25 insertions(+), 9 deletions(-) + +diff --git a/src/twisted/words/test/test_jabberclient.py b/src/twisted/words/test/test_jabberclient.py +index 2e39de72cee..8afb92951f7 100644 +--- a/src/twisted/words/test/test_jabberclient.py ++++ b/src/twisted/words/test/test_jabberclient.py +@@ -9,13 +9,21 @@ + + from hashlib import sha1 + +-from twisted.internet import defer, ssl ++from twisted.internet import defer + from twisted.python.compat import unicode + from twisted.trial import unittest + from twisted.words.protocols.jabber import client, error, jid, xmlstream + from twisted.words.protocols.jabber.sasl import SASLInitiatingInitializer + from twisted.words.xish import utility + ++try: ++ from twisted.internet import ssl ++except ImportError: ++ ssl = None ++ skipWhenNoSSL = "SSL not available" ++else: ++ skipWhenNoSSL = None ++ + IQ_AUTH_GET = '/iq[@type="get"]/query[@xmlns="jabber:iq:auth"]' + IQ_AUTH_SET = '/iq[@type="set"]/query[@xmlns="jabber:iq:auth"]' + NS_BIND = 'urn:ietf:params:xml:ns:xmpp-bind' +@@ -478,3 +486,5 @@ def test_tlsContextFactory(self): + + self.assertIsInstance(tls, xmlstream.TLSInitiatingInitializer) + self.assertIs(contextFactory, tls.contextFactory) ++ ++ test_tlsContextFactory.skip = skipWhenNoSSL +diff --git a/src/twisted/words/test/test_jabberxmlstream.py b/src/twisted/words/test/test_jabberxmlstream.py +index d9f4962ec0c..aad0305ef99 100644 +--- a/src/twisted/words/test/test_jabberxmlstream.py ++++ b/src/twisted/words/test/test_jabberxmlstream.py +@@ -14,8 +14,6 @@ + from twisted.internet import defer, task + from twisted.internet.error import ConnectionLost + from twisted.internet.interfaces import IProtocolFactory +-from twisted.internet.ssl import CertificateOptions +-from twisted.internet._sslverify import ClientTLSOptions + from twisted.python import failure + from twisted.python.compat import unicode + from twisted.test import proto_helpers +@@ -23,7 +21,15 @@ + from twisted.words.xish import domish + from twisted.words.protocols.jabber import error, ijabber, jid, xmlstream + +- ++try: ++ from twisted.internet import ssl ++except ImportError: ++ ssl = None ++ skipWhenNoSSL = "SSL not available" ++else: ++ skipWhenNoSSL = None ++ from twisted.internet.ssl import CertificateOptions ++ from twisted.internet._sslverify import ClientTLSOptions + + NS_XMPP_TLS = 'urn:ietf:params:xml:ns:xmpp-tls' + +@@ -710,6 +716,8 @@ def test_wantedSupported(self): + + return d + ++ test_wantedSupported.skip = skipWhenNoSSL ++ + + def test_certificateVerify(self): + """ +@@ -731,6 +739,8 @@ def fakeStartTLS(contextFactory): + self.assertEqual(['TLS', 'reset', 'header'], self.done) + return d + ++ test_certificateVerify.skip = skipWhenNoSSL ++ + + def test_certificateVerifyContext(self): + """ +@@ -753,11 +763,7 @@ def fakeStartTLS(contextFactory): + self.assertEqual(['TLS', 'reset', 'header'], self.done) + return d + +- +- if not xmlstream.ssl: +- test_wantedSupported.skip = "SSL not available" +- test_certificateVerify = "SSL not available" +- test_certificateVerifyContext = "SSL not available" ++ test_certificateVerifyContext.skip = skipWhenNoSSL + + + def test_wantedNotSupportedNotRequired(self): + diff -Nru twisted-18.9.0/debian/patches/CVE-2019-12855-11.patch twisted-18.9.0/debian/patches/CVE-2019-12855-11.patch --- twisted-18.9.0/debian/patches/CVE-2019-12855-11.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2019-12855-11.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,46 @@ +From 672a6338dea08a17cbe18af3d47bdb14fcd0d84b Mon Sep 17 00:00:00 2001 +From: Ralph Meijer +Date: Mon, 27 May 2019 15:33:20 +0200 +Subject: [PATCH 11/17] Fix indents + +--- + src/twisted/words/test/test_jabberclient.py | 4 ++-- + src/twisted/words/test/test_jabberxmlstream.py | 2 +- + 2 files changed, 3 insertions(+), 3 deletions(-) + +diff --git a/src/twisted/words/test/test_jabberclient.py b/src/twisted/words/test/test_jabberclient.py +index 8afb92951f7..7c31bed8656 100644 +--- a/src/twisted/words/test/test_jabberclient.py ++++ b/src/twisted/words/test/test_jabberclient.py +@@ -17,7 +17,7 @@ + from twisted.words.xish import utility + + try: +- from twisted.internet import ssl ++ from twisted.internet import ssl + except ImportError: + ssl = None + skipWhenNoSSL = "SSL not available" +@@ -406,7 +406,7 @@ def test_basic(self): + # XMPPAuthenticator (that has its associateWithXmlStream called) that + # is in turn initialized with the arguments to the factory. + xs = client.basicClientFactory(self.client_jid, +- 'secret').buildProtocol(None) ++ 'secret').buildProtocol(None) + + # test authenticator's instance variables + self.assertEqual('example.com', xs.authenticator.otherHost) +diff --git a/src/twisted/words/test/test_jabberxmlstream.py b/src/twisted/words/test/test_jabberxmlstream.py +index aad0305ef99..7b384645a2c 100644 +--- a/src/twisted/words/test/test_jabberxmlstream.py ++++ b/src/twisted/words/test/test_jabberxmlstream.py +@@ -22,7 +22,7 @@ + from twisted.words.protocols.jabber import error, ijabber, jid, xmlstream + + try: +- from twisted.internet import ssl ++ from twisted.internet import ssl + except ImportError: + ssl = None + skipWhenNoSSL = "SSL not available" + diff -Nru twisted-18.9.0/debian/patches/CVE-2019-12855-12.patch twisted-18.9.0/debian/patches/CVE-2019-12855-12.patch --- twisted-18.9.0/debian/patches/CVE-2019-12855-12.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2019-12855-12.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,34 @@ +From a649757186c12d2b4f4a8e215b4d36ba26bd331f Mon Sep 17 00:00:00 2001 +From: Ralph Meijer +Date: Tue, 28 May 2019 16:53:22 +0200 +Subject: [PATCH 12/17] Better docstring for BasicAuthenticatorTests + +--- + src/twisted/words/test/test_jabberclient.py | 13 ++++++++----- + 1 file changed, 8 insertions(+), 5 deletions(-) + +diff --git a/src/twisted/words/test/test_jabberclient.py b/src/twisted/words/test/test_jabberclient.py +index 7c31bed8656..1403131baf6 100644 +--- a/src/twisted/words/test/test_jabberclient.py ++++ b/src/twisted/words/test/test_jabberclient.py +@@ -394,11 +394,14 @@ class BasicAuthenticatorTests(unittest.TestCase): + + def test_basic(self): + """ +- Test basic operations. +- +- Setup a basicClientFactory, which sets up a BasicAuthenticator, and let +- it produce a protocol instance. Then inspect the instance variables of +- the authenticator and XML stream objects. ++ Authenticator and stream are properly constructed by the factory. ++ ++ The L{xmlstream.XmlStream} protocol created by the factory has the new ++ L{client.BasicAuthenticator} instance in its C{authenticator} ++ attribute. It is set up with C{jid} and C{password} as passed to the ++ factory, C{otherHost} taken from the client JID. The stream futher has ++ two initializers, for TLS and authentication, of which the first has ++ its C{required} attribute set to C{True}. + """ + self.client_jid = jid.JID('user@example.com/resource') + + diff -Nru twisted-18.9.0/debian/patches/CVE-2019-12855-13.patch twisted-18.9.0/debian/patches/CVE-2019-12855-13.patch --- twisted-18.9.0/debian/patches/CVE-2019-12855-13.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2019-12855-13.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,211 @@ +From ea2d28f7035cdbc56063a0672acef426086875ff Mon Sep 17 00:00:00 2001 +From: Ralph Meijer +Date: Sun, 16 Jun 2019 18:41:49 +0200 +Subject: [PATCH 13/17] Rename contextFactory to configurationForTLS, make + private vars + +--- + src/twisted/words/newsfragments/9561.feature | 2 +- + src/twisted/words/protocols/jabber/client.py | 37 +++++++++++-------- + .../words/protocols/jabber/xmlstream.py | 28 +++++++------- + src/twisted/words/test/test_jabberclient.py | 26 +++++++------ + .../words/test/test_jabberxmlstream.py | 3 ++ + 5 files changed, 55 insertions(+), 41 deletions(-) + +diff --git a/src/twisted/words/protocols/jabber/client.py b/src/twisted/words/protocols/jabber/client.py +index 4b310e34f38..db4cbfccf21 100644 +--- a/src/twisted/words/protocols/jabber/client.py ++++ b/src/twisted/words/protocols/jabber/client.py +@@ -298,7 +298,7 @@ def start(self): + + + +-def XMPPClientFactory(jid, password, contextFactory=None): ++def XMPPClientFactory(jid, password, configurationForTLS=None): + """ + Client factory for XMPP 1.0 (only). + +@@ -314,16 +314,18 @@ def XMPPClientFactory(jid, password, contextFactory=None): + @param password: password to authenticate with. + @type password: L{unicode} + +- @param contextFactory: An object which creates appropriately configured TLS +- connections. This is passed to C{startTLS} on the transport and is +- preferably created using L{twisted.internet.ssl.optionsForClientTLS}. +- See L{xmlstream.TLSInitiatingInitializer} for details. +- @type contextFactory: L{IOpenSSLClientConnectionCreator} ++ @param configurationForTLS: An object which creates appropriately ++ configured TLS connections. This is passed to C{startTLS} on the ++ transport and is preferably created using ++ L{twisted.internet.ssl.optionsForClientTLS}. See ++ L{xmlstream.TLSInitiatingInitializer} for details. ++ @type configurationForTLS: L{IOpenSSLClientConnectionCreator} + + @return: XML stream factory. + @rtype: L{xmlstream.XmlStreamFactory} + """ +- a = XMPPAuthenticator(jid, password, contextFactory=contextFactory) ++ a = XMPPAuthenticator(jid, password, ++ configurationForTLS=configurationForTLS) + return xmlstream.XmlStreamFactory(a) + + +@@ -361,21 +363,23 @@ class XMPPAuthenticator(xmlstream.ConnectAuthenticator): + + @ivar password: password to be used during SASL authentication. + @type password: L{unicode} +- +- @ivar contextFactory: An object which creates appropriately configured TLS +- connections. This is passed to C{startTLS} on the transport and is +- preferably created using L{twisted.internet.ssl.optionsForClientTLS}. +- See L{xmlstream.TLSInitiatingInitializer} for details. +- @type contextFactory: L{IOpenSSLClientConnectionCreator} + """ + + namespace = 'jabber:client' + +- def __init__(self, jid, password, contextFactory=None): ++ def __init__(self, jid, password, configurationForTLS=None): ++ """ ++ @param configurationForTLS: An object which creates appropriately ++ configured TLS connections. This is passed to C{startTLS} on the ++ transport and is preferably created using ++ L{twisted.internet.ssl.optionsForClientTLS}. See ++ L{xmlstream.TLSInitiatingInitializer} for details. ++ @type configurationForTLS: L{IOpenSSLClientConnectionCreator} ++ """ + xmlstream.ConnectAuthenticator.__init__(self, jid.host) + self.jid = jid + self.password = password +- self.contextFactory = contextFactory ++ self._configurationForTLS = configurationForTLS + + + def associateWithStream(self, xs): +@@ -392,7 +396,8 @@ def associateWithStream(self, xs): + xs.initializers = [ + CheckVersionInitializer(xs), + xmlstream.TLSInitiatingInitializer( +- xs, required=True, contextFactory=self.contextFactory), ++ xs, required=True, ++ configurationForTLS=self._configurationForTLS), + sasl.SASLInitiatingInitializer(xs, required=True), + BindInitializer(xs, required=True), + SessionInitializer(xs, required=False), +diff --git a/src/twisted/words/protocols/jabber/xmlstream.py b/src/twisted/words/protocols/jabber/xmlstream.py +index 1ed79d47726..135d71295df 100644 +--- a/src/twisted/words/protocols/jabber/xmlstream.py ++++ b/src/twisted/words/protocols/jabber/xmlstream.py +@@ -403,25 +403,27 @@ class TLSInitiatingInitializer(BaseFeatureInitiatingInitializer): + + @ivar wanted: indicates if TLS negotiation is wanted. + @type wanted: C{bool} +- +- @ivar contextFactory: An object which creates appropriately configured TLS +- connections. This is passed to C{startTLS} on the transport and is +- preferably created using L{twisted.internet.ssl.optionsForClientTLS}. +- If C{None}, the default is to verify the server certificate against +- the trust roots as provided by the platform. See +- L{twisted.internet._sslverify.platformTrust}. +- @type contextFactory: L{IOpenSSLClientConnectionCreator} + """ + + feature = (NS_XMPP_TLS, 'starttls') + wanted = True +- contextFactory = None + _deferred = None ++ _configurationForTLS = None + +- def __init__(self, xs, required=True, contextFactory=None): ++ def __init__(self, xs, required=True, configurationForTLS=None): ++ """ ++ @param configurationForTLS: An object which creates appropriately ++ configured TLS connections. This is passed to C{startTLS} on the ++ transport and is preferably created using ++ L{twisted.internet.ssl.optionsForClientTLS}. If C{None}, the ++ default is to verify the server certificate against the trust roots ++ as provided by the platform. See ++ L{twisted.internet._sslverify.platformTrust}. ++ @type configurationForTLS: L{IOpenSSLClientConnectionCreator} ++ """ + super(TLSInitiatingInitializer, self).__init__( + xs, required=required) +- self.contextFactory = contextFactory ++ self._configurationForTLS = configurationForTLS + + + def onProceed(self, obj): +@@ -430,8 +432,8 @@ def onProceed(self, obj): + """ + + self.xmlstream.removeObserver('/failure', self.onFailure) +- if self.contextFactory: +- ctx = self.contextFactory ++ if self._configurationForTLS: ++ ctx = self._configurationForTLS + else: + ctx = ssl.optionsForClientTLS(self.xmlstream.otherEntity.host) + self.xmlstream.transport.startTLS(ctx) +diff --git a/src/twisted/words/test/test_jabberclient.py b/src/twisted/words/test/test_jabberclient.py +index 1403131baf6..4f5c8092419 100644 +--- a/src/twisted/words/test/test_jabberclient.py ++++ b/src/twisted/words/test/test_jabberclient.py +@@ -466,28 +466,32 @@ def test_basic(self): + self.assertFalse(session.required) + + +- def test_tlsContextFactory(self): ++ def test_tlsConfiguration(self): + """ +- Test basic operations. +- +- Setup an XMPPClientFactory, which sets up an XMPPAuthenticator, and let +- it produce a protocol instance. Then inspect the instance variables of +- the authenticator and XML stream objects. ++ A TLS configuration is passed to the TLS initializer. + """ ++ configs = [] ++ ++ def init(self, xs, required=True, configurationForTLS=None): ++ configs.append(configurationForTLS) ++ + self.client_jid = jid.JID('user@example.com/resource') + + # Get an XmlStream instance. Note that it gets initialized with the + # XMPPAuthenticator (that has its associateWithXmlStream called) that + # is in turn initialized with the arguments to the factory. +- contextFactory = ssl.CertificateOptions() +- factory = client.XMPPClientFactory(self.client_jid, 'secret', +- contextFactory=contextFactory) ++ configurationForTLS = ssl.CertificateOptions() ++ factory = client.XMPPClientFactory( ++ self.client_jid, 'secret', ++ configurationForTLS=configurationForTLS) ++ self.patch(xmlstream.TLSInitiatingInitializer, "__init__", init) + xs = factory.buildProtocol(None) + + # test list of initializers + version, tls, sasl, bind, session = xs.initializers + + self.assertIsInstance(tls, xmlstream.TLSInitiatingInitializer) +- self.assertIs(contextFactory, tls.contextFactory) ++ self.assertIs(configurationForTLS, configs[0]) ++ + +- test_tlsContextFactory.skip = skipWhenNoSSL ++ test_tlsConfiguration.skip = skipWhenNoSSL +diff --git a/src/twisted/words/test/test_jabberxmlstream.py b/src/twisted/words/test/test_jabberxmlstream.py +index 7b384645a2c..85f6d195d4a 100644 +--- a/src/twisted/words/test/test_jabberxmlstream.py ++++ b/src/twisted/words/test/test_jabberxmlstream.py +@@ -747,6 +747,9 @@ def test_certificateVerifyContext(self): + A custom contextFactory is passed through to startTLS. + """ + ctx = CertificateOptions() ++ self.init = xmlstream.TLSInitiatingInitializer( ++ self.xmlstream, configurationForTLS=ctx) ++ + self.init.contextFactory = ctx + + def fakeStartTLS(contextFactory): + diff -Nru twisted-18.9.0/debian/patches/CVE-2019-12855-14.patch twisted-18.9.0/debian/patches/CVE-2019-12855-14.patch --- twisted-18.9.0/debian/patches/CVE-2019-12855-14.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2019-12855-14.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,40 @@ +From 05556b6ca14a49e4c7f3b5e8ede83137b869926e Mon Sep 17 00:00:00 2001 +From: Ralph Meijer +Date: Sun, 16 Jun 2019 19:02:52 +0200 +Subject: [PATCH 14/17] Move check for configurationTLS being None to __init__ + +--- + src/twisted/words/protocols/jabber/xmlstream.py | 12 ++++++------ + 1 file changed, 6 insertions(+), 6 deletions(-) + +diff --git a/src/twisted/words/protocols/jabber/xmlstream.py b/src/twisted/words/protocols/jabber/xmlstream.py +index 135d71295df..dd4bd8f1932 100644 +--- a/src/twisted/words/protocols/jabber/xmlstream.py ++++ b/src/twisted/words/protocols/jabber/xmlstream.py +@@ -423,7 +423,11 @@ def __init__(self, xs, required=True, configurationForTLS=None): + """ + super(TLSInitiatingInitializer, self).__init__( + xs, required=required) +- self._configurationForTLS = configurationForTLS ++ if configurationForTLS: ++ self._configurationForTLS = configurationForTLS ++ else: ++ self._configurationForTLS = ssl.optionsForClientTLS( ++ self.xmlstream.authenticator.otherHost) + + + def onProceed(self, obj): +@@ -432,11 +436,7 @@ def onProceed(self, obj): + """ + + self.xmlstream.removeObserver('/failure', self.onFailure) +- if self._configurationForTLS: +- ctx = self._configurationForTLS +- else: +- ctx = ssl.optionsForClientTLS(self.xmlstream.otherEntity.host) +- self.xmlstream.transport.startTLS(ctx) ++ self.xmlstream.transport.startTLS(self._configurationForTLS) + self.xmlstream.reset() + self.xmlstream.sendHeader() + self._deferred.callback(Reset) + diff -Nru twisted-18.9.0/debian/patches/CVE-2019-12855-15.patch twisted-18.9.0/debian/patches/CVE-2019-12855-15.patch --- twisted-18.9.0/debian/patches/CVE-2019-12855-15.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2019-12855-15.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,59 @@ +From 7caf8ac8795492e346e8f52633ff6d343a07edde Mon Sep 17 00:00:00 2001 +From: Ralph Meijer +Date: Sun, 16 Jun 2019 19:11:35 +0200 +Subject: [PATCH 15/17] Document configurationForTLS being None directly + +--- + src/twisted/words/protocols/jabber/client.py | 16 ++++++++++------ + src/twisted/words/protocols/jabber/xmlstream.py | 3 ++- + 2 files changed, 12 insertions(+), 7 deletions(-) + +diff --git a/src/twisted/words/protocols/jabber/client.py b/src/twisted/words/protocols/jabber/client.py +index db4cbfccf21..8f197cdafe1 100644 +--- a/src/twisted/words/protocols/jabber/client.py ++++ b/src/twisted/words/protocols/jabber/client.py +@@ -317,9 +317,10 @@ def XMPPClientFactory(jid, password, configurationForTLS=None): + @param configurationForTLS: An object which creates appropriately + configured TLS connections. This is passed to C{startTLS} on the + transport and is preferably created using +- L{twisted.internet.ssl.optionsForClientTLS}. See +- L{xmlstream.TLSInitiatingInitializer} for details. +- @type configurationForTLS: L{IOpenSSLClientConnectionCreator} ++ L{twisted.internet.ssl.optionsForClientTLS}. If C{None}, the default is ++ to verify the server certificate against the trust roots as provided by ++ the platform. See L{twisted.internet._sslverify.platformTrust}. ++ @type configurationForTLS: L{IOpenSSLClientConnectionCreator} or C{None} + + @return: XML stream factory. + @rtype: L{xmlstream.XmlStreamFactory} +@@ -372,9 +373,12 @@ def __init__(self, jid, password, configurationForTLS=None): + @param configurationForTLS: An object which creates appropriately + configured TLS connections. This is passed to C{startTLS} on the + transport and is preferably created using +- L{twisted.internet.ssl.optionsForClientTLS}. See +- L{xmlstream.TLSInitiatingInitializer} for details. +- @type configurationForTLS: L{IOpenSSLClientConnectionCreator} ++ L{twisted.internet.ssl.optionsForClientTLS}. If C{None}, the ++ default is to verify the server certificate against the trust roots ++ as provided by the platform. See ++ L{twisted.internet._sslverify.platformTrust}. ++ @type configurationForTLS: L{IOpenSSLClientConnectionCreator} or ++ C{None} + """ + xmlstream.ConnectAuthenticator.__init__(self, jid.host) + self.jid = jid +diff --git a/src/twisted/words/protocols/jabber/xmlstream.py b/src/twisted/words/protocols/jabber/xmlstream.py +index dd4bd8f1932..905402c5360 100644 +--- a/src/twisted/words/protocols/jabber/xmlstream.py ++++ b/src/twisted/words/protocols/jabber/xmlstream.py +@@ -419,7 +419,8 @@ def __init__(self, xs, required=True, configurationForTLS=None): + default is to verify the server certificate against the trust roots + as provided by the platform. See + L{twisted.internet._sslverify.platformTrust}. +- @type configurationForTLS: L{IOpenSSLClientConnectionCreator} ++ @type configurationForTLS: L{IOpenSSLClientConnectionCreator} or ++ C{None} + """ + super(TLSInitiatingInitializer, self).__init__( + xs, required=required) + diff -Nru twisted-18.9.0/debian/patches/CVE-2019-12855-17.patch twisted-18.9.0/debian/patches/CVE-2019-12855-17.patch --- twisted-18.9.0/debian/patches/CVE-2019-12855-17.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2019-12855-17.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,41 @@ +From abbf0fd52c13b1fb5e1429189a3fcc48565870a5 Mon Sep 17 00:00:00 2001 +From: Ralph Meijer +Date: Sun, 16 Jun 2019 19:50:33 +0200 +Subject: [PATCH 17/17] Revert "Move check for configurationTLS being None to + __init__" + +This reverts commit 05556b6ca14a49e4c7f3b5e8ede83137b869926e. +--- + src/twisted/words/protocols/jabber/xmlstream.py | 12 ++++++------ + 1 file changed, 6 insertions(+), 6 deletions(-) + +diff --git a/src/twisted/words/protocols/jabber/xmlstream.py b/src/twisted/words/protocols/jabber/xmlstream.py +index 905402c5360..20948c6d3be 100644 +--- a/src/twisted/words/protocols/jabber/xmlstream.py ++++ b/src/twisted/words/protocols/jabber/xmlstream.py +@@ -424,11 +424,7 @@ def __init__(self, xs, required=True, configurationForTLS=None): + """ + super(TLSInitiatingInitializer, self).__init__( + xs, required=required) +- if configurationForTLS: +- self._configurationForTLS = configurationForTLS +- else: +- self._configurationForTLS = ssl.optionsForClientTLS( +- self.xmlstream.authenticator.otherHost) ++ self._configurationForTLS = configurationForTLS + + + def onProceed(self, obj): +@@ -437,7 +433,11 @@ def onProceed(self, obj): + """ + + self.xmlstream.removeObserver('/failure', self.onFailure) +- self.xmlstream.transport.startTLS(self._configurationForTLS) ++ if self._configurationForTLS: ++ ctx = self._configurationForTLS ++ else: ++ ctx = ssl.optionsForClientTLS(self.xmlstream.otherEntity.host) ++ self.xmlstream.transport.startTLS(ctx) + self.xmlstream.reset() + self.xmlstream.sendHeader() + self._deferred.callback(Reset) diff -Nru twisted-18.9.0/debian/patches/CVE-2019-951x.patch twisted-18.9.0/debian/patches/CVE-2019-951x.patch --- twisted-18.9.0/debian/patches/CVE-2019-951x.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2019-951x.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,627 @@ +Backport of: + +From 1595d9adc21c580065d1d6036c9611c411990816 Mon Sep 17 00:00:00 2001 +From: Mark Williams +Date: Thu, 8 Aug 2019 19:18:20 -0700 +Subject: [PATCH] Buffer outbound control frames and timeout invalid clients. + +A HTTP/2 server can be effectively DoSed by having a remote peer stop +reading from a connection while continuing to send frames that trigger +automatic control frame emission. This patch addresses that by ensuring +that rather than automatically write all control frames into the +transport, we will buffer them in the HTTP/2 connection object, ensuring +that we have visibility into the size of that buffer, and thus can abort +the connection if it grows too large. + +An HTTP/2 server can also be DoSed by a client that sends only invalid +frames (e.g., a RESET_STREAM frame when no streams have been created.) +This patches addresses that by only resetting H2Connection's timeout +when the underlying h2.connection.H2Connection has parsed at least one +valid frame. +--- + src/twisted/web/_http2.py | 130 ++++++++++---- + src/twisted/web/error.py | 8 + + src/twisted/web/http.py | 8 +- + src/twisted/web/test/test_http.py | 13 +- + src/twisted/web/test/test_http2.py | 262 ++++++++++++++++++++++++++++- + 5 files changed, 387 insertions(+), 34 deletions(-) + +--- a/src/twisted/web/_http2.py ++++ b/src/twisted/web/_http2.py +@@ -41,6 +41,7 @@ from twisted.internet.protocol import Pr + from twisted.logger import Logger + from twisted.protocols.policies import TimeoutMixin + from twisted.python.failure import Failure ++from twisted.web.error import ExcessiveBufferingError + + + # This API is currently considered private. +@@ -138,6 +139,12 @@ class H2Connection(Protocol, TimeoutMixi + self._streamCleanupCallbacks = {} + self._stillProducing = True + ++ # Limit the number of buffered control frame (e.g. PING and ++ # SETTINGS) bytes. ++ self._maxBufferedControlFrameBytes = 1024 * 17 ++ self._bufferedControlFrames = deque() ++ self._bufferedControlFrameBytes = 0 ++ + if reactor is None: + from twisted.internet import reactor + self._reactor = reactor +@@ -165,18 +172,19 @@ class H2Connection(Protocol, TimeoutMixi + @param data: The data received from the transport. + @type data: L{bytes} + """ +- self.resetTimeout() +- + try: + events = self.conn.receive_data(data) + except h2.exceptions.ProtocolError: +- # A remote protocol error terminates the connection. +- dataToSend = self.conn.data_to_send() +- self.transport.write(dataToSend) +- self.transport.loseConnection() +- self.connectionLost(Failure()) ++ stillActive = self._tryToWriteControlData() ++ if stillActive: ++ self.transport.loseConnection() ++ self.connectionLost(Failure(), _cancelTimeouts=False) + return + ++ # Only reset the timeout if we've received an actual H2 ++ # protocol message ++ self.resetTimeout() ++ + for event in events: + if isinstance(event, h2.events.RequestReceived): + self._requestReceived(event) +@@ -192,11 +200,12 @@ class H2Connection(Protocol, TimeoutMixi + self._handlePriorityUpdate(event) + elif isinstance(event, h2.events.ConnectionTerminated): + self.transport.loseConnection() +- self.connectionLost(ConnectionLost("Remote peer sent GOAWAY")) ++ self.connectionLost( ++ ConnectionLost("Remote peer sent GOAWAY"), ++ _cancelTimeouts=False, ++ ) + +- dataToSend = self.conn.data_to_send() +- if dataToSend: +- self.transport.write(dataToSend) ++ self._tryToWriteControlData() + + + def timeoutConnection(self): +@@ -259,15 +268,23 @@ class H2Connection(Protocol, TimeoutMixi + self.transport.abortConnection() + + +- def connectionLost(self, reason): ++ def connectionLost(self, reason, _cancelTimeouts=True): + """ + Called when the transport connection is lost. + +- Informs all outstanding response handlers that the connection has been +- lost, and cleans up all internal state. ++ Informs all outstanding response handlers that the connection ++ has been lost, and cleans up all internal state. ++ ++ @param reason: See L{IProtocol.connectionLost} ++ ++ @param _cancelTimeouts: Propagate the C{reason} to this ++ connection's streams but don't cancel any timers, so that ++ peers who never read the data we've written are eventually ++ timed out. + """ + self._stillProducing = False +- self.setTimeout(None) ++ if _cancelTimeouts: ++ self.setTimeout(None) + + for stream in self.streams.values(): + stream.connectionLost(reason) +@@ -276,7 +293,7 @@ class H2Connection(Protocol, TimeoutMixi + self._requestDone(streamID) + + # If we were going to force-close the transport, we don't have to now. +- if self._abortingCall is not None: ++ if _cancelTimeouts and self._abortingCall is not None: + self._abortingCall.cancel() + self._abortingCall = None + +@@ -324,7 +341,8 @@ class H2Connection(Protocol, TimeoutMixi + # + # Note that all of this only applies to *data*. Headers and other control + # frames deliberately skip this processing as they are not subject to flow +- # control or priority constraints. ++ # control or priority constraints. Instead, they are stored in their own buffer ++ # which is used primarily to detect excessive buffering. + def stopProducing(self): + """ + Stop producing data. +@@ -343,6 +361,8 @@ class H2Connection(Protocol, TimeoutMixi + for the time being, and to stop until resumeProducing() is called. + """ + self._consumerBlocked = Deferred() ++ # Ensure pending control data (if any) are sent first. ++ self._consumerBlocked.addCallback(self._flushBufferedControlData) + + + def resumeProducing(self): +@@ -568,7 +588,7 @@ class H2Connection(Protocol, TimeoutMixi + # when a connection is lost, so that's what we do too. + return + else: +- self.transport.write(self.conn.data_to_send()) ++ self._tryToWriteControlData() + + + def writeDataToStream(self, streamID, data): +@@ -622,8 +642,9 @@ class H2Connection(Protocol, TimeoutMixi + @type streamID: L{int} + """ + self.conn.reset_stream(streamID) +- self.transport.write(self.conn.data_to_send()) +- self._requestDone(streamID) ++ stillActive = self._tryToWriteControlData() ++ if stillActive: ++ self._requestDone(streamID) + + + def _requestDone(self, streamID): +@@ -739,9 +760,7 @@ class H2Connection(Protocol, TimeoutMixi + @type increment: L{int} + """ + self.conn.acknowledge_received_data(increment, streamID) +- data = self.conn.data_to_send() +- if data: +- self.transport.write(data) ++ self._tryToWriteControlData() + + + def _isSecure(self): +@@ -766,7 +785,7 @@ class H2Connection(Protocol, TimeoutMixi + """ + headers = [(b':status', b'100')] + self.conn.send_headers(headers=headers, stream_id=streamID) +- self.transport.write(self.conn.data_to_send()) ++ self._tryToWriteControlData() + + + def _respondToBadRequestAndDisconnect(self, streamID): +@@ -791,11 +810,11 @@ class H2Connection(Protocol, TimeoutMixi + stream_id=streamID, + end_stream=True + ) +- self.transport.write(self.conn.data_to_send()) +- +- stream = self.streams[streamID] +- stream.connectionLost(ConnectionLost("Invalid request")) +- self._requestDone(streamID) ++ stillActive = self._tryToWriteControlData() ++ if stillActive: ++ stream = self.streams[streamID] ++ stream.connectionLost(ConnectionLost("Invalid request")) ++ self._requestDone(streamID) + + + def _streamIsActive(self, streamID): +@@ -811,6 +830,59 @@ class H2Connection(Protocol, TimeoutMixi + """ + return streamID in self.streams + ++ def _tryToWriteControlData(self): ++ """ ++ Checks whether the connection is blocked on flow control and, ++ if it isn't, writes any buffered control data. ++ ++ @return: L{True} if the connection is still active and ++ L{False} if it was aborted because too many bytes have ++ been written but not consumed by the other end. ++ """ ++ bufferedBytes = self.conn.data_to_send() ++ if not bufferedBytes: ++ return True ++ ++ if self._consumerBlocked is None and not self._bufferedControlFrames: ++ # The consumer isn't blocked, and we don't have any buffered frames: ++ # write this directly. ++ self.transport.write(bufferedBytes) ++ return True ++ else: ++ # Either the consumer is blocked or we have buffered frames. If the ++ # consumer is blocked, we'll write this when we unblock. If we have ++ # buffered frames, we have presumably been re-entered from ++ # transport.write, and so to avoid reordering issues we'll buffer anyway. ++ self._bufferedControlFrames.append(bufferedBytes) ++ self._bufferedControlFrameBytes += len(bufferedBytes) ++ ++ if self._bufferedControlFrameBytes >= self._maxBufferedControlFrameBytes: ++ self._log.error( ++ "Maximum number of control frame bytes buffered: " ++ "{bufferedControlFrameBytes} > = {maxBufferedControlFrameBytes}. " ++ "Aborting connection to client: {client} ", ++ bufferedControlFrameBytes=self._bufferedControlFrameBytes, ++ maxBufferedControlFrameBytes=self._maxBufferedControlFrameBytes, ++ client=self.transport.getPeer(), ++ ) ++ # We've exceeded a reasonable buffer size for max buffered control frames. ++ # This is a denial of service risk, so we're going to drop this connection. ++ self.transport.abortConnection() ++ self.connectionLost(ExcessiveBufferingError()) ++ return False ++ return True ++ ++ def _flushBufferedControlData(self, *args): ++ """ ++ Called when the connection is marked writable again after being marked unwritable. ++ Attempts to flush buffered control data if there is any. ++ """ ++ # To respect backpressure here we send each write in order, paying attention to whether ++ # we got blocked ++ while self._consumerBlocked is None and self._bufferedControlFrames: ++ nextWrite = self._bufferedControlFrames.popleft() ++ self._bufferedControlFrameBytes -= len(nextWrite) ++ self.transport.write(nextWrite) + + + @implementer(ITransport, IConsumer, IPushProducer) +--- a/src/twisted/web/error.py ++++ b/src/twisted/web/error.py +@@ -300,6 +300,14 @@ class UnsupportedType(Exception): + """ + + ++class ExcessiveBufferingError(Exception): ++ """ ++ The HTTP/2 protocol has been forced to buffer an excessive amount of ++ outbound data, and has therefore closed the connection and dropped all ++ outbound data. ++ """ ++ ++ + + class FlattenerError(Exception): + """ +--- a/src/twisted/web/http.py ++++ b/src/twisted/web/http.py +@@ -2892,7 +2892,8 @@ class _GenericHTTPChannelProtocol(proxyF + # We need to make sure that the HTTPChannel is unregistered + # from the transport so that the H2Connection can register + # itself if possible. +- self._channel._networkProducer.unregisterProducer() ++ networkProducer = self._channel._networkProducer ++ networkProducer.unregisterProducer() + + transport = self._channel.transport + self._channel = H2Connection() +@@ -2902,6 +2903,11 @@ class _GenericHTTPChannelProtocol(proxyF + self._channel.timeOut = self._timeOut + self._channel.callLater = self._callLater + self._channel.makeConnection(transport) ++ ++ # Register the H2Connection as the transport's ++ # producer, so that the transport can apply back ++ # pressure. ++ networkProducer.registerProducer(self._channel, True) + else: + # Only HTTP/2 and HTTP/1.1 are supported right now. + assert negotiatedProtocol == b'http/1.1', \ +--- a/src/twisted/web/test/test_http.py ++++ b/src/twisted/web/test/test_http.py +@@ -896,15 +896,22 @@ class GenericHTTPChannelTests(unittest.T + genericProtocol.requestFactory = DummyHTTPHandlerProxy + genericProtocol.makeConnection(transport) + ++ originalChannel = genericProtocol._channel ++ + # We expect the transport has a underlying channel registered as + # a producer. +- self.assertIs(transport.producer, genericProtocol._channel) ++ self.assertIs(transport.producer, originalChannel) + + # Force the upgrade. + genericProtocol.dataReceived(b'P') + +- # The transport should now have no producer. +- self.assertIs(transport.producer, None) ++ # The transport should not have the original channel as its ++ # producer... ++ self.assertIsNot(transport.producer, originalChannel) ++ ++ # ...it should have the new H2 channel as its producer ++ self.assertIs(transport.producer, genericProtocol._channel) ++ + if not http.H2_ENABLED: + test_unregistersProducer.skip = "HTTP/2 support not present" + +--- a/src/twisted/web/test/test_http2.py ++++ b/src/twisted/web/test/test_http2.py +@@ -14,7 +14,7 @@ from zope.interface import providedBy, d + from twisted.internet import defer, reactor, task, error + from twisted.python import failure + from twisted.python.compat import iterbytes +-from twisted.test.proto_helpers import StringTransport ++from twisted.test.proto_helpers import StringTransport, MemoryReactorClock + from twisted.test.test_internet import DummyProducer + from twisted.trial import unittest + from twisted.web import http +@@ -1651,6 +1651,194 @@ class HTTP2ServerTests(unittest.TestCase + return d + + ++ def test_fast400WithCircuitBreaker(self): ++ """ ++ A HTTP/2 stream that has had _respondToBadRequestAndDisconnect ++ called on it does not write control frame data if its ++ transport is paused and its control frame limit has been ++ reached. ++ """ ++ # Set the connection up. ++ memoryReactor = MemoryReactorClock() ++ connection = H2Connection(memoryReactor) ++ connection.callLater = memoryReactor.callLater ++ # Use the DelayedHTTPHandler to prevent the connection from ++ # writing any response bytes after receiving a request that ++ # establishes the stream. ++ connection.requestFactory = DelayedHTTPHandler ++ ++ streamID = 1 ++ ++ frameFactory = FrameFactory() ++ transport = StringTransport() ++ ++ # Establish the connection ++ clientConnectionPreface = frameFactory.clientConnectionPreface() ++ connection.makeConnection(transport) ++ connection.dataReceived(clientConnectionPreface) ++ # Establish the stream. ++ connection.dataReceived( ++ buildRequestBytes( ++ self.getRequestHeaders, [], frameFactory, streamID=streamID) ++ ) ++ ++ # Pause the connection and limit the number of outbound bytes ++ # to 0, so that attempting to send the 400 aborts the ++ # connection. ++ connection.pauseProducing() ++ connection._maxBufferedControlFrameBytes = 0 ++ ++ connection._respondToBadRequestAndDisconnect(streamID) ++ ++ self.assertTrue(transport.disconnected) ++ ++ ++ def test_bufferingAutomaticFrameData(self): ++ """ ++ If a the L{H2Connection} has been paused by the transport, it will ++ not write automatic frame data triggered by writes. ++ """ ++ # Set the connection up. ++ connection = H2Connection() ++ connection.requestFactory = DummyHTTPHandlerProxy ++ frameFactory = FrameFactory() ++ transport = StringTransport() ++ ++ clientConnectionPreface = frameFactory.clientConnectionPreface() ++ connection.makeConnection(transport) ++ connection.dataReceived(clientConnectionPreface) ++ ++ # Now we're going to pause the producer. ++ connection.pauseProducing() ++ ++ # Now we're going to send a bunch of empty SETTINGS frames. This ++ # should not cause writes. ++ for _ in range(0, 100): ++ connection.dataReceived(frameFactory.buildSettingsFrame({}).serialize()) ++ ++ frames = framesFromBytes(transport.value()) ++ self.assertEqual(len(frames), 1) ++ ++ # Re-enable the transport. ++ connection.resumeProducing() ++ frames = framesFromBytes(transport.value()) ++ self.assertEqual(len(frames), 101) ++ ++ ++ def test_bufferingAutomaticFrameDataWithCircuitBreaker(self): ++ """ ++ If the L{H2Connection} has been paused by the transport, it will ++ not write automatic frame data triggered by writes. If this buffer ++ gets too large, the connection will be dropped. ++ """ ++ # Set the connection up. ++ connection = H2Connection() ++ connection.requestFactory = DummyHTTPHandlerProxy ++ frameFactory = FrameFactory() ++ transport = StringTransport() ++ ++ clientConnectionPreface = frameFactory.clientConnectionPreface() ++ connection.makeConnection(transport) ++ connection.dataReceived(clientConnectionPreface) ++ ++ # Now we're going to pause the producer. ++ connection.pauseProducing() ++ ++ # Now we're going to limit the outstanding buffered bytes to ++ # 100 bytes. ++ connection._maxBufferedControlFrameBytes = 100 ++ ++ # Now we're going to send 11 empty SETTINGS frames. This ++ # should not cause writes, or a close. ++ self.assertFalse(transport.disconnecting) ++ for _ in range(0, 11): ++ connection.dataReceived(frameFactory.buildSettingsFrame({}).serialize()) ++ self.assertFalse(transport.disconnecting) ++ ++ # Send a last settings frame, which will push us over the buffer limit. ++ connection.dataReceived(frameFactory.buildSettingsFrame({}).serialize()) ++ self.assertTrue(transport.disconnected) ++ ++ ++ def test_bufferingContinuesIfProducerIsPausedOnWrite(self): ++ """ ++ If the L{H2Connection} has buffered control frames, is unpaused, and then ++ paused while unbuffering, it persists the buffer and stops trying to write. ++ """ ++ class AutoPausingStringTransport(StringTransport): ++ def write(self, *args, **kwargs): ++ StringTransport.write(self, *args, **kwargs) ++ self.producer.pauseProducing() ++ ++ # Set the connection up. ++ connection = H2Connection() ++ connection.requestFactory = DummyHTTPHandlerProxy ++ frameFactory = FrameFactory() ++ transport = AutoPausingStringTransport() ++ transport.registerProducer(connection, True) ++ ++ clientConnectionPreface = frameFactory.clientConnectionPreface() ++ connection.makeConnection(transport) ++ connection.dataReceived(clientConnectionPreface) ++ ++ # The connection should already be paused. ++ self.assertIsNotNone(connection._consumerBlocked) ++ frames = framesFromBytes(transport.value()) ++ self.assertEqual(len(frames), 1) ++ self.assertEqual(connection._bufferedControlFrameBytes, 0) ++ ++ # Now we're going to send 11 empty SETTINGS frames. This should produce ++ # no output, but some buffered settings ACKs. ++ for _ in range(0, 11): ++ connection.dataReceived(frameFactory.buildSettingsFrame({}).serialize()) ++ ++ frames = framesFromBytes(transport.value()) ++ self.assertEqual(len(frames), 1) ++ self.assertEqual(connection._bufferedControlFrameBytes, 9 * 11) ++ ++ # Ok, now we're going to unpause the producer. This should write only one of the ++ # SETTINGS ACKs, as the connection gets repaused. ++ connection.resumeProducing() ++ ++ frames = framesFromBytes(transport.value()) ++ self.assertEqual(len(frames), 2) ++ self.assertEqual(connection._bufferedControlFrameBytes, 9 * 10) ++ ++ ++ def test_circuitBreakerAbortsAfterProtocolError(self): ++ """ ++ A client that triggers a L{h2.exceptions.ProtocolError} over a ++ paused connection that's reached its buffered control frame ++ limit causes that connection to be aborted. ++ """ ++ memoryReactor = MemoryReactorClock() ++ connection = H2Connection(memoryReactor) ++ connection.callLater = memoryReactor.callLater ++ ++ frameFactory = FrameFactory() ++ transport = StringTransport() ++ ++ # Establish the connection. ++ clientConnectionPreface = frameFactory.clientConnectionPreface() ++ connection.makeConnection(transport) ++ connection.dataReceived(clientConnectionPreface) ++ ++ # Pause it and limit the number of outbound bytes to 0, so ++ # that a ProtocolError aborts the connection ++ connection.pauseProducing() ++ connection._maxBufferedControlFrameBytes = 0 ++ ++ # Trigger a ProtocolError with a data frame that refers to an ++ # unknown stream. ++ invalidData = frameFactory.buildDataFrame( ++ data=b'yo', streamID=0xF0 ++ ).serialize() ++ ++ # The frame should have aborted the connection. ++ connection.dataReceived(invalidData) ++ self.assertTrue(transport.disconnected) ++ ++ + + class H2FlowControlTests(unittest.TestCase, HTTP2TestHelpers): + """ +@@ -2364,6 +2552,50 @@ class H2FlowControlTests(unittest.TestCa + self.assertFalse(dataFrames) + + ++ def test_abortRequestWithCircuitBreaker(self): ++ """ ++ Aborting a request associated with a paused connection that's ++ reached its buffered control frame limit causes that ++ connection to be aborted. ++ """ ++ memoryReactor = MemoryReactorClock() ++ connection = H2Connection(memoryReactor) ++ connection.callLater = memoryReactor.callLater ++ connection.requestFactory = DummyHTTPHandlerProxy ++ ++ frameFactory = FrameFactory() ++ transport = StringTransport() ++ ++ # Establish the connection. ++ clientConnectionPreface = frameFactory.clientConnectionPreface() ++ connection.makeConnection(transport) ++ connection.dataReceived(clientConnectionPreface) ++ ++ # Send a headers frame for a stream ++ streamID = 1 ++ headersFrameData = frameFactory.buildHeadersFrame( ++ headers=self.postRequestHeaders, streamID=streamID ++ ).serialize() ++ connection.dataReceived(headersFrameData) ++ ++ # Pause it and limit the number of outbound bytes to 1, so ++ # that a ProtocolError aborts the connection ++ connection.pauseProducing() ++ connection._maxBufferedControlFrameBytes = 0 ++ ++ # Remove anything sent by the preceding frames. ++ transport.clear() ++ ++ # Abort the request. ++ connection.abortRequest(streamID) ++ ++ # No RST_STREAM frame was sent... ++ self.assertFalse(transport.value()) ++ # ...and the transport was disconnected (abortConnection was ++ # called) ++ self.assertTrue(transport.disconnected) ++ ++ + + class HTTP2TransportChecking(unittest.TestCase, HTTP2TestHelpers): + getRequestHeaders = [ +@@ -2901,3 +3133,31 @@ class HTTP2TimeoutTests(unittest.TestCas + # transports, including TCP and TLS. We don't have anything we can + # assert on here: this just must not explode. + conn.connectionLost(error.ConnectionDone) ++ ++ ++ def test_timeOutClientThatSendsOnlyInvalidFrames(self): ++ """ ++ A client that sends only invalid frames is eventually timed out. ++ """ ++ memoryReactor = MemoryReactorClock() ++ ++ connection = H2Connection(memoryReactor) ++ connection.callLater = memoryReactor.callLater ++ connection.timeOut = 60 ++ ++ frameFactory = FrameFactory() ++ transport = StringTransport() ++ ++ clientConnectionPreface = frameFactory.clientConnectionPreface() ++ connection.makeConnection(transport) ++ connection.dataReceived(clientConnectionPreface) ++ ++ # Send data until both the loseConnection and abortConnection ++ # timeouts have elapsed. ++ for _ in range(connection.timeOut + connection.abortTimeout): ++ connection.dataReceived(frameFactory.buildRstStreamFrame(1).serialize()) ++ memoryReactor.advance(1) ++ ++ # Invalid frames don't reset any timeouts, so the above has ++ # forcibly disconnected us via abortConnection. ++ self.assertTrue(transport.disconnected) diff -Nru twisted-18.9.0/debian/patches/CVE-2020-1010x-pre1.patch twisted-18.9.0/debian/patches/CVE-2020-1010x-pre1.patch --- twisted-18.9.0/debian/patches/CVE-2020-1010x-pre1.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2020-1010x-pre1.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,135 @@ +Backport of: + +From d2f6dd9b3766509f40c980aac67ca8475da67c6f Mon Sep 17 00:00:00 2001 +From: Tom Most +Date: Mon, 3 Jun 2019 22:03:22 -0700 +Subject: [PATCH] Refactor to reduce duplication + +--- + src/twisted/web/test/test_http.py | 116 +++++++++++------------------- + 1 file changed, 42 insertions(+), 74 deletions(-) + +--- a/src/twisted/web/test/test_http.py ++++ b/src/twisted/web/test/test_http.py +@@ -1370,7 +1370,8 @@ class ParsingTests(unittest.TestCase): + """ + Execute a web request based on plain text content. + +- @param httpRequest: Content for the request which is processed. ++ @param httpRequest: Content for the request which is processed. Each ++ L{"\n"} will be replaced with L{"\r\n"}. + @type httpRequest: C{bytes} + + @param requestFactory: 2-argument callable returning a Request. +@@ -1409,6 +1410,32 @@ class ParsingTests(unittest.TestCase): + return channel + + ++ def assertRequestRejected(self, requestLines): ++ """ ++ Execute a HTTP request and assert that it is rejected with a 400 Bad ++ Response and disconnection. ++ ++ @param requestLines: Plain text lines of the request. These lines will ++ be joined with newlines to form the HTTP request that is processed. ++ @type requestLines: C{list} of C{bytes} ++ """ ++ httpRequest = b"\n".join(requestLines) ++ processed = [] ++ ++ class MyRequest(http.Request): ++ def process(self): ++ processed.append(self) ++ self.finish() ++ ++ channel = self.runRequest(httpRequest, MyRequest, success=False) ++ self.assertEqual( ++ channel.transport.value(), ++ b"HTTP/1.1 400 Bad Request\r\n\r\n", ++ ) ++ self.assertTrue(channel.transport.disconnecting) ++ self.assertEqual(processed, []) ++ ++ + def test_invalidNonAsciiMethod(self): + """ + When client sends invalid HTTP method containing +@@ -1478,45 +1505,24 @@ class ParsingTests(unittest.TestCase): + + def test_tooManyHeaders(self): + """ +- L{HTTPChannel} enforces a limit of C{HTTPChannel.maxHeaders} on the ++ C{HTTPChannel} enforces a limit of C{HTTPChannel.maxHeaders} on the + number of headers received per request. + """ +- processed = [] +- class MyRequest(http.Request): +- def process(self): +- processed.append(self) +- + requestLines = [b"GET / HTTP/1.0"] + for i in range(http.HTTPChannel.maxHeaders + 2): + requestLines.append(networkString("%s: foo" % (i,))) + requestLines.extend([b"", b""]) + +- channel = self.runRequest(b"\n".join(requestLines), MyRequest, 0) +- self.assertEqual(processed, []) +- self.assertEqual( +- channel.transport.value(), +- b"HTTP/1.1 400 Bad Request\r\n\r\n") ++ self.assertRequestRejected(requestLines) + + + def test_invalidContentLengthHeader(self): + """ +- If a Content-Length header with a non-integer value is received, a 400 +- (Bad Request) response is sent to the client and the connection is +- closed. ++ If a I{Content-Length} header with a non-integer value is received, ++ a 400 (Bad Request) response is sent to the client and the connection ++ is closed. + """ +- processed = [] +- class MyRequest(http.Request): +- def process(self): +- processed.append(self) +- self.finish() +- +- requestLines = [b"GET / HTTP/1.0", b"Content-Length: x", b"", b""] +- channel = self.runRequest(b"\n".join(requestLines), MyRequest, 0) +- self.assertEqual( +- channel.transport.value(), +- b"HTTP/1.1 400 Bad Request\r\n\r\n") +- self.assertTrue(channel.transport.disconnecting) +- self.assertEqual(processed, []) ++ self.assertRequestRejected([b"GET / HTTP/1.0", b"Content-Length: x", b"", b""]) + + + def test_invalidHeaderNoColon(self): +@@ -1524,24 +1530,12 @@ class ParsingTests(unittest.TestCase): + If a header without colon is received a 400 (Bad Request) response + is sent to the client and the connection is closed. + """ +- processed = [] +- class MyRequest(http.Request): +- def process(self): +- processed.append(self) +- self.finish() +- +- requestLines = [b"GET / HTTP/1.0", b"HeaderName ", b"", b""] +- channel = self.runRequest(b"\n".join(requestLines), MyRequest, 0) +- self.assertEqual( +- channel.transport.value(), +- b"HTTP/1.1 400 Bad Request\r\n\r\n") +- self.assertTrue(channel.transport.disconnecting) +- self.assertEqual(processed, []) ++ self.assertRequestRejected([b"GET / HTTP/1.0", b"HeaderName ", b"", b""]) + + + def test_headerLimitPerRequest(self): + """ +- L{HTTPChannel} enforces the limit of C{HTTPChannel.maxHeaders} per ++ C{HTTPChannel} enforces the limit of C{HTTPChannel.maxHeaders} per + request so that headers received in an earlier request do not count + towards the limit when processing a later request. + """ diff -Nru twisted-18.9.0/debian/patches/CVE-2020-1010x.patch twisted-18.9.0/debian/patches/CVE-2020-1010x.patch --- twisted-18.9.0/debian/patches/CVE-2020-1010x.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2020-1010x.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,248 @@ +From 4a7d22e490bb8ff836892cc99a1f54b85ccb0281 Mon Sep 17 00:00:00 2001 +From: Mark Williams +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 + +--- a/src/twisted/web/http.py ++++ b/src/twisted/web/http.py +@@ -2115,6 +2115,51 @@ class HTTPChannel(basic.LineReceiver, po + 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): + """ +@@ -2136,21 +2181,10 @@ class HTTPChannel(basic.LineReceiver, po + + 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: +--- a/src/twisted/web/test/test_http.py ++++ b/src/twisted/web/test/test_http.py +@@ -2018,6 +2018,143 @@ Hello, + 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): diff -Nru twisted-18.9.0/debian/patches/CVE-2022-21712-1.patch twisted-18.9.0/debian/patches/CVE-2022-21712-1.patch --- twisted-18.9.0/debian/patches/CVE-2022-21712-1.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2022-21712-1.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,262 @@ +[PATCH 1/10]: Backport of the following upstream patch +From eda4f1e2ec9988a142de244f1a2b285939718c03 Mon Sep 17 00:00:00 2001 +From: Glyph +Date: Sun, 23 Jan 2022 12:57:49 -0800 +Subject: [PATCH] failing test for header data leak + +--- + src/twisted/web/test/test_agent.py | 154 +++++++++++++++++++++++------ + 1 file changed, 123 insertions(+), 31 deletions(-) + +--- a/src/twisted/web/test/test_agent.py ++++ b/src/twisted/web/test/test_agent.py +@@ -5,12 +5,18 @@ + Tests for L{twisted.web.client.Agent} and related new client APIs. + """ + +-import zlib +- + from io import BytesIO ++from twisted.test.iosim import FakeTransport, IOPump ++from twisted.test.proto_helpers import ( ++ AccumulatingProtocol, ++ EventLoggingObserver, ++ MemoryReactorClock, ++ StringTransport, ++) ++from twisted.test.test_sslverify import certificatesForAuthorityAndServer ++from unittest import skipIf, SkipTest + +-from zope.interface.verify import verifyObject +- ++import zlib + from twisted.trial.unittest import TestCase, SynchronousTestCase + from twisted.web import client, error, http_headers + from twisted.web._newclient import RequestNotSent, RequestTransmissionFailed +@@ -20,8 +26,6 @@ from twisted.internet import defer, task + from twisted.python.failure import Failure + from twisted.python.compat import cookielib, intToBytes + from twisted.python.components import proxyForInterface +-from twisted.test.proto_helpers import (StringTransport, MemoryReactorClock, +- EventLoggingObserver) + from twisted.internet.task import Clock + from twisted.internet.error import ConnectionRefusedError, ConnectionDone + from twisted.internet.error import ConnectionLost +@@ -30,32 +34,42 @@ from twisted.internet.defer import Defer + from twisted.internet.endpoints import TCP4ClientEndpoint + from twisted.internet.address import IPv4Address, IPv6Address + +-from twisted.web.client import (FileBodyProducer, Request, HTTPConnectionPool, +- ResponseDone, _HTTP11ClientFactory, URI) +- ++from twisted.web.client import ( ++ BrowserLikePolicyForHTTPS, ++ FileBodyProducer, ++ HTTPConnectionPool, ++ Request, ++ ResponseDone, ++ URI, ++ _HTTP11ClientFactory, ++) + from twisted.web.iweb import ( +- UNKNOWN_LENGTH, IAgent, IBodyProducer, IResponse, IAgentEndpointFactory, +- ) ++ IAgent, ++ IAgentEndpointFactory, ++ IBodyProducer, ++ IPolicyForHTTPS, ++ IRequest, ++ IResponse, ++ UNKNOWN_LENGTH, ++) ++ + from twisted.web.http_headers import Headers + from twisted.web._newclient import HTTP11ClientProtocol, Response + + from twisted.internet.interfaces import IOpenSSLClientConnectionCreator +-from zope.interface.declarations import implementer +-from twisted.web.iweb import IPolicyForHTTPS + from twisted.python.deprecate import getDeprecationWarningString + from incremental import Version +-from twisted.web.client import BrowserLikePolicyForHTTPS + from twisted.internet.test.test_endpoints import deterministicResolvingReactor + from twisted.internet.endpoints import HostnameEndpoint +-from twisted.test.proto_helpers import AccumulatingProtocol +-from twisted.test.iosim import IOPump, FakeTransport +-from twisted.test.test_sslverify import certificatesForAuthorityAndServer + from twisted.web.test.injectionhelpers import ( + MethodInjectionTestsMixin, + URIInjectionTestsMixin, + ) + from twisted.web.error import SchemeNotSupported + from twisted.logger import globalLogPublisher ++from zope.interface.declarations import implementer ++from zope.interface.verify import verifyObject ++ + + try: + from twisted.internet import ssl +@@ -2606,8 +2620,21 @@ class ProxyAgentTests(TestCase, FakeReac + self.assertEqual(agent._pool.connected, True) + + ++SENSITIVE_HEADERS = [ ++ b"authorization", ++ b"cookie", ++ b"cookie2", ++ b"proxy-authorization", ++ b"www-authenticate", ++] + +-class _RedirectAgentTestsMixin(object): ++if False: # TYPE_CHECKING ++ testMixinClass = TestCase ++else: ++ testMixinClass = object ++ ++ ++class _RedirectAgentTestsMixin(testMixinClass): + """ + Test cases mixin for L{RedirectAgentTests} and + L{BrowserLikeRedirectAgentTests}. +@@ -2631,33 +2658,52 @@ class _RedirectAgentTestsMixin(object): + self.assertIdentical(result.previousResponse, None) + + +- def _testRedirectDefault(self, code): ++ def _testRedirectDefault( ++ self, ++ code, ++ crossScheme = False, ++ crossDomain = False, ++ requestHeaders = None, ++ ): + """ + When getting a redirect, L{client.RedirectAgent} follows the URL + specified in the L{Location} header field and make a new request. + + @param code: HTTP status code. + """ +- self.agent.request(b'GET', b'http://example.com/foo') ++ startDomain = b"example.com" ++ startScheme = b"https" if ssl is not None else b"http" ++ startPort = 80 if startScheme == b"http" else 443 ++ self.agent.request( ++ b"GET", startScheme + b"://" + startDomain + b"/foo", headers=requestHeaders ++ ) + + host, port = self.reactor.tcpClients.pop()[:2] + self.assertEqual(EXAMPLE_COM_IP, host) +- self.assertEqual(80, port) ++ self.assertEqual(startPort, port) + + req, res = self.protocol.requests.pop() + +- # If possible (i.e.: SSL support is present), run the test with a ++ # If possible (i.e.: TLS support is present), run the test with a + # cross-scheme redirect to verify that the scheme is honored; if not, + # let's just make sure it works at all. +- if ssl is None: +- scheme = b'http' +- expectedPort = 80 +- else: +- scheme = b'https' +- expectedPort = 443 + ++ targetScheme = startScheme ++ targetDomain = startDomain ++ targetPort = startPort ++ ++ if crossDomain: ++ if ssl is None: ++ raise SkipTest( ++ "Cross-scheme redirects can't be tested without TLS support." ++ ) ++ targetScheme = b"https" if startScheme == b"http" else b"https" ++ targetPort = 443 if startPort == 80 else 443 ++ ++ targetDomain = b"example.net" if crossDomain else startDomain + headers = http_headers.Headers( +- {b'location': [scheme + b'://example.com/bar']}) ++ {b"location": [targetScheme + b"://" + targetDomain + b"/bar"]} ++ ) + response = Response((b'HTTP', 1, 1), code, b'OK', headers, None) + res.callback(response) + +@@ -2666,8 +2712,9 @@ class _RedirectAgentTestsMixin(object): + self.assertEqual(b'/bar', req2.uri) + + host, port = self.reactor.tcpClients.pop()[:2] +- self.assertEqual(EXAMPLE_COM_IP, host) +- self.assertEqual(expectedPort, port) ++ self.assertEqual(EXAMPLE_NET_IP if crossDomain else EXAMPLE_COM_IP, host) ++ self.assertEqual(targetPort, port) ++ return req2 + + + def test_redirect301(self): +@@ -2677,6 +2724,15 @@ class _RedirectAgentTestsMixin(object): + self._testRedirectDefault(301) + + ++ def test_redirect301Scheme(self): ++ """ ++ L{client.RedirectAgent} follows cross-scheme redirects. ++ """ ++ self._testRedirectDefault( ++ 301, ++ crossScheme=True, ++ ) ++ + def test_redirect302(self): + """ + L{client.RedirectAgent} follows redirects on status code 302. +@@ -2691,6 +2747,48 @@ class _RedirectAgentTestsMixin(object): + self._testRedirectDefault(307) + + ++ def test_headerSecurity(self): ++ """ ++ L{client.RedirectAgent} scrubs sensitive headers when redirecting. ++ """ ++ sensitiveHeaderValues = { ++ b"authorization": [b"sensitive-authnz"], ++ b"cookie": [b"sensitive-cookie-data"], ++ b"cookie2": [b"sensitive-cookie2-data"], ++ b"proxy-authorization": [b"sensitive-proxy-auth"], ++ b"wWw-auThentiCate": [b"sensitive-authn"], ++ } ++ otherHeaderValues = {b"x-random-header": [b"x-random-value"]} ++ soh = sensitiveHeaderValues.copy() ++ soh.update(otherHeaderValues) ++ allHeaders = Headers(soh) ++ redirected = self._testRedirectDefault(301, requestHeaders=allHeaders) ++ ++ def normHeaders(headers): ++ return {k.lower(): v for (k, v) in headers.getAllRawHeaders()} ++ ++ sameOriginHeaders = normHeaders(redirected.headers) ++ t = {b"host": [b"example.com"]} ++ t.update(normHeaders(allHeaders)) ++ self.assertEquals( ++ sameOriginHeaders, ++ t, ++ ) ++ ++ redirectedElsewhere = self._testRedirectDefault( ++ 301, ++ crossDomain=True, ++ requestHeaders=Headers(soh), ++ ) ++ otherOriginHeaders = normHeaders(redirectedElsewhere.headers) ++ t = {b"host": [b"example.com"]} ++ t.update(normHeaders(Headers(otherHeaderValues))) ++ self.assertEquals( ++ otherOriginHeaders, ++ t, ++ ) ++ ++ + def _testRedirectToGet(self, code, method): + """ + L{client.RedirectAgent} changes the method to I{GET} when getting diff -Nru twisted-18.9.0/debian/patches/CVE-2022-21712-10.patch twisted-18.9.0/debian/patches/CVE-2022-21712-10.patch --- twisted-18.9.0/debian/patches/CVE-2022-21712-10.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2022-21712-10.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,29 @@ +From 0c44b4806a27d258baf13d6f714f06eddb28da5a Mon Sep 17 00:00:00 2001 +From: Glyph +Date: Sun, 23 Jan 2022 15:31:51 -0800 +Subject: [PATCH] correct docstring to suggest the right order + +--- + src/twisted/web/iweb.py | 10 +++++----- + 1 file changed, 5 insertions(+), 5 deletions(-) + +--- a/src/twisted/web/iweb.py ++++ b/src/twisted/web/iweb.py +@@ -682,12 +682,12 @@ class IAgent(Interface): + obtained by combining a number of (hypothetical) implementations:: + + baseAgent = Agent(reactor) +- redirect = BrowserLikeRedirectAgent(baseAgent, limit=10) ++ decode = ContentDecoderAgent(baseAgent, [(b"gzip", GzipDecoder())]) ++ cookie = CookieAgent(decode, diskStore.cookie) + authenticate = AuthenticateAgent( +- redirect, [diskStore.credentials, GtkAuthInterface()]) +- cookie = CookieAgent(authenticate, diskStore.cookie) +- decode = ContentDecoderAgent(cookie, [(b"gzip", GzipDecoder())]) +- cache = CacheAgent(decode, diskStore.cache) ++ cookie, [diskStore.credentials, GtkAuthInterface()]) ++ cache = CacheAgent(authenticate, diskStore.cache) ++ redirect = BrowserLikeRedirectAgent(cache, limit=10) + + doSomeRequests(cache) + """ diff -Nru twisted-18.9.0/debian/patches/CVE-2022-21712-2.patch twisted-18.9.0/debian/patches/CVE-2022-21712-2.patch --- twisted-18.9.0/debian/patches/CVE-2022-21712-2.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2022-21712-2.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,21 @@ +[PATCH 2/10]: Backport of the following upstream patch +From ecc2ae81c831e58cf1725dfe2e5b6d2951c884a0 Mon Sep 17 00:00:00 2001 +From: Glyph +Date: Sun, 23 Jan 2022 13:00:02 -0800 +Subject: [PATCH] assert on correct host + +--- + src/twisted/web/test/test_agent.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +--- a/src/twisted/web/test/test_agent.py ++++ b/src/twisted/web/test/test_agent.py +@@ -2781,7 +2781,7 @@ + requestHeaders=Headers(soh), + ) + otherOriginHeaders = normHeaders(redirectedElsewhere.headers) +- t = {b"host": [b"example.com"]} ++ t = {b"host": [b"example.net"]} + t.update(normHeaders(Headers(otherHeaderValues))) + self.assertEquals( + otherOriginHeaders, diff -Nru twisted-18.9.0/debian/patches/CVE-2022-21712-3.patch twisted-18.9.0/debian/patches/CVE-2022-21712-3.patch --- twisted-18.9.0/debian/patches/CVE-2022-21712-3.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2022-21712-3.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,235 @@ +From c1923d24b6a2752ea5d5686851427e0ec5757543 Mon Sep 17 00:00:00 2001 +From: Glyph +Date: Sun, 23 Jan 2022 14:04:27 -0800 +Subject: [PATCH] tests for domain/port/scheme and fix + +--- + src/twisted/web/client.py | 61 +++++++++++++++++++++--------- + src/twisted/web/test/test_agent.py | 52 ++++++++++++++++++++----- + 2 files changed, 86 insertions(+), 27 deletions(-) + +--- a/src/twisted/web/client.py ++++ b/src/twisted/web/client.py +@@ -24,16 +24,12 @@ + import zlib + from functools import wraps + +-from zope.interface import implementer +- + from twisted.python.compat import _PY3, networkString + from twisted.python.compat import nativeString, intToBytes, unicode, itervalues +-from twisted.python.deprecate import deprecatedModuleAttribute, deprecated ++from twisted.python.deprecate import deprecated, deprecatedModuleAttribute, getDeprecationWarningString + from twisted.python.failure import Failure + from incremental import Version + +-from twisted.web.iweb import IPolicyForHTTPS, IAgentEndpointFactory +-from twisted.python.deprecate import getDeprecationWarningString + from twisted.web import http + from twisted.internet import defer, protocol, task, reactor + from twisted.internet.abstract import isIPv6Address +@@ -42,7 +38,9 @@ + from twisted.python.util import InsensitiveDict + from twisted.python.components import proxyForInterface + from twisted.web import error +-from twisted.web.iweb import UNKNOWN_LENGTH, IAgent, IBodyProducer, IResponse ++from twisted.web.iweb import IAgent, IAgentEndpointFactory, IBodyProducer, IPolicyForHTTPS, IResponse, UNKNOWN_LENGTH ++from zope.interface import implementer ++ + from twisted.web.http_headers import Headers + from twisted.logger import Logger + +@@ -2012,6 +2010,14 @@ + return response + + ++_canonicalHeaderName = Headers()._canonicalNameCaps ++_defaultSensitiveHeaders = frozenset([ ++ b"Authorization", ++ b"Cookie", ++ b"Cookie2", ++ b"Proxy-Authorization", ++ b"WWW-Authenticate", ++]) + + @implementer(IAgent) + class RedirectAgent(object): +@@ -2027,6 +2033,11 @@ + @param redirectLimit: The maximum number of times the agent is allowed to + follow redirects before failing with a L{error.InfiniteRedirection}. + ++ @param sensitiveHeaderNames: An iterable of C{bytes} enumerating the names ++ of headers that must not be transmitted when redirecting to a different ++ origins. These will be consulted in addition to the protocol-specified ++ set of headers that contain sensitive information. ++ + @cvar _redirectResponses: A L{list} of HTTP status codes to be redirected + for I{GET} and I{HEAD} methods. + +@@ -2041,9 +2052,17 @@ + _seeOtherResponses = [http.SEE_OTHER] + + +- def __init__(self, agent, redirectLimit=20): ++ def __init__( ++ self, ++ agent, ++ redirectLimit = 20, ++ sensitiveHeaderNames = (), ++ ): + self._agent = agent + self._redirectLimit = redirectLimit ++ sensitive = set(_canonicalHeaderName(each) for each in sensitiveHeaderNames) ++ sensitive.update(_defaultSensitiveHeaders) ++ self._sensitiveHeaderNames = sensitive + + + def request(self, method, uri, headers=None, bodyProducer=None): +@@ -2090,6 +2109,22 @@ + response.code, b'No location header field', uri) + raise ResponseFailed([Failure(err)], response) + location = self._resolveLocation(uri, locationHeaders[0]) ++ if headers: ++ parsedURI = URI.fromBytes(uri) ++ parsedLocation = URI.fromBytes(location) ++ sameOrigin = ( ++ (parsedURI.scheme == parsedLocation.scheme) ++ and (parsedURI.host == parsedLocation.host) ++ and (parsedURI.port == parsedLocation.port) ++ ) ++ if not sameOrigin: ++ headers = Headers( ++ { ++ rawName: rawValue ++ for rawName, rawValue in headers.getAllRawHeaders() ++ if rawName not in self._sensitiveHeaderNames ++ } ++ ) + deferred = self._agent.request(method, location, headers) + def _chainResponse(newResponse): + newResponse.setPreviousResponse(response) +--- a/src/twisted/web/test/test_agent.py ++++ b/src/twisted/web/test/test_agent.py +@@ -2663,6 +2663,7 @@ + code, + crossScheme = False, + crossDomain = False, ++ crossPort = False, + requestHeaders = None, + ): + """ +@@ -2692,17 +2693,22 @@ + targetDomain = startDomain + targetPort = startPort + +- if crossDomain: ++ if crossScheme: + if ssl is None: + raise SkipTest( + "Cross-scheme redirects can't be tested without TLS support." + ) +- targetScheme = b"https" if startScheme == b"http" else b"https" +- targetPort = 443 if startPort == 80 else 443 +- ++ targetScheme = b"https" if startScheme == b"http" else b"http" ++ targetPort = 443 if startPort == 80 else 80 ++ ++ portSyntax = b'' ++ if crossPort: ++ targetPort = 8443 ++ portSyntax = b':8443' + targetDomain = b"example.net" if crossDomain else startDomain ++ locationValue = targetScheme + b"://" + targetDomain + portSyntax + b"/bar" + headers = http_headers.Headers( +- {b"location": [targetScheme + b"://" + targetDomain + b"/bar"]} ++ {b"location": [locationValue]} + ) + response = Response((b'HTTP', 1, 1), code, b'OK', headers, None) + res.callback(response) +@@ -2747,9 +2753,10 @@ + self._testRedirectDefault(307) + + +- def test_headerSecurity(self): ++ def _sensitiveHeadersTest(self, expectedHostHeader = b"example.com", **crossKwargs): + """ +- L{client.RedirectAgent} scrubs sensitive headers when redirecting. ++ L{client.RedirectAgent} scrubs sensitive headers when redirecting ++ between differing origins. + """ + sensitiveHeaderValues = { + b"authorization": [b"sensitive-authnz"], +@@ -2757,6 +2764,7 @@ + b"cookie2": [b"sensitive-cookie2-data"], + b"proxy-authorization": [b"sensitive-proxy-auth"], + b"wWw-auThentiCate": [b"sensitive-authn"], ++ b"x-custom-sensitive": [b"sensitive-custom"], + } + otherHeaderValues = {b"x-random-header": [b"x-random-value"]} + soh = sensitiveHeaderValues.copy() +@@ -2777,17 +2785,37 @@ + + redirectedElsewhere = self._testRedirectDefault( + 301, +- crossDomain=True, + requestHeaders=Headers(soh), ++ **crossKwargs + ) + otherOriginHeaders = normHeaders(redirectedElsewhere.headers) +- t = {b"host": [b"example.net"]} ++ t = {b"host": [expectedHostHeader]} + t.update(normHeaders(Headers(otherHeaderValues))) + self.assertEquals( + otherOriginHeaders, + t, + ) + ++ def test_crossDomainHeaders(self): ++ """ ++ L{client.RedirectAgent} scrubs sensitive headers when redirecting ++ between differing domains. ++ """ ++ self._sensitiveHeadersTest(crossDomain=True, expectedHostHeader=b'example.net') ++ ++ def test_crossPortHeaders(self): ++ """ ++ L{client.RedirectAgent} scrubs sensitive headers when redirecting ++ between differing ports. ++ """ ++ self._sensitiveHeadersTest(crossPort=True, expectedHostHeader=b'example.com:8443') ++ ++ def test_crossSchemeHeaders(self): ++ """ ++ L{client.RedirectAgent} scrubs sensitive headers when redirecting ++ between differing schemes. ++ """ ++ self._sensitiveHeadersTest(crossScheme=True) + + def _testRedirectToGet(self, code, method): + """ +@@ -3014,8 +3042,10 @@ + @return: a new L{twisted.web.client.RedirectAgent} + """ + return client.RedirectAgent( +- self.buildAgentForWrapperTest(self.reactor)) +- ++ self.buildAgentForWrapperTest(self.reactor), ++ sensitiveHeaderNames=[b"X-Custom-sensitive"], ++ ) ++ + + def setUp(self): + self.reactor = self.createReactor() +@@ -3053,8 +3083,10 @@ + @return: a new L{twisted.web.client.BrowserLikeRedirectAgent} + """ + return client.BrowserLikeRedirectAgent( +- self.buildAgentForWrapperTest(self.reactor)) +- ++ self.buildAgentForWrapperTest(self.reactor), ++ sensitiveHeaderNames=[b"x-Custom-sensitive"], ++ ) ++ + + def setUp(self): + self.reactor = self.createReactor() diff -Nru twisted-18.9.0/debian/patches/CVE-2022-21712-4.patch twisted-18.9.0/debian/patches/CVE-2022-21712-4.patch --- twisted-18.9.0/debian/patches/CVE-2022-21712-4.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2022-21712-4.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,15 @@ +From 562dcff9c7dd846857932443a0a2935f74c2a2de Mon Sep 17 00:00:00 2001 +From: Glyph +Date: Sun, 23 Jan 2022 14:11:46 -0800 +Subject: [PATCH] topfile + +--- + src/twisted/topfiles/10294.bugfix | 1 + + 1 file changed, 1 insertion(+) + create mode 100644 src/twisted/topfiles/10294.bugfix + +--- /dev/null ++++ b/src/twisted/topfiles/10294.bugfix +@@ -0,0 +1 @@ ++twisted.web.client.RedirectAgent and twisted.web.client.BrowserLikeRedirectAgent now properly remove sensitive headers when redirecting to a different origin. +\ No newline at end of file diff -Nru twisted-18.9.0/debian/patches/CVE-2022-21712-5.patch twisted-18.9.0/debian/patches/CVE-2022-21712-5.patch --- twisted-18.9.0/debian/patches/CVE-2022-21712-5.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2022-21712-5.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,218 @@ +From 4ae6732a30893178891ee3a383d8a737e0ec21eb Mon Sep 17 00:00:00 2001 +From: Glyph +Date: Sun, 23 Jan 2022 14:16:18 -0800 +Subject: [PATCH] reblackening + +--- + src/twisted/web/client.py | 41 +++++++++++++++-------- + src/twisted/web/test/test_agent.py | 52 ++++++++++++++++-------------- + 2 files changed, 55 insertions(+), 38 deletions(-) + +--- a/src/twisted/web/client.py ++++ b/src/twisted/web/client.py +@@ -10,6 +10,7 @@ + + import os + import warnings ++import zlib + + try: + from urlparse import urlunparse, urljoin, urldefrag +@@ -21,12 +22,17 @@ + result = _urlunparse(tuple([p.decode("charmap") for p in parts])) + return result.encode("charmap") + +-import zlib ++from zope.interface import implementer ++ + from functools import wraps + + from twisted.python.compat import _PY3, networkString + from twisted.python.compat import nativeString, intToBytes, unicode, itervalues +-from twisted.python.deprecate import deprecated, deprecatedModuleAttribute, getDeprecationWarningString ++from twisted.python.deprecate import ( ++ deprecated, ++ deprecatedModuleAttribute, ++ getDeprecationWarningString, ++) + from twisted.python.failure import Failure + from incremental import Version + +@@ -38,12 +44,17 @@ + from twisted.python.util import InsensitiveDict + from twisted.python.components import proxyForInterface + from twisted.web import error +-from twisted.web.iweb import IAgent, IAgentEndpointFactory, IBodyProducer, IPolicyForHTTPS, IResponse, UNKNOWN_LENGTH +-from zope.interface import implementer ++from twisted.web.iweb import ( ++ UNKNOWN_LENGTH, ++ IAgent, ++ IAgentEndpointFactory, ++ IBodyProducer, ++ IPolicyForHTTPS, ++ IResponse, ++) + + from twisted.web.http_headers import Headers + from twisted.logger import Logger +- + from twisted.web._newclient import _ensureValidURI, _ensureValidMethod + + +@@ -2011,13 +2022,16 @@ + + + _canonicalHeaderName = Headers()._canonicalNameCaps +-_defaultSensitiveHeaders = frozenset([ +- b"Authorization", +- b"Cookie", +- b"Cookie2", +- b"Proxy-Authorization", +- b"WWW-Authenticate", +-]) ++_defaultSensitiveHeaders = frozenset( ++ [ ++ b"Authorization", ++ b"Cookie", ++ b"Cookie2", ++ b"Proxy-Authorization", ++ b"WWW-Authenticate", ++ ] ++) ++ + + @implementer(IAgent) + class RedirectAgent(object): +@@ -2060,7 +2074,7 @@ + ): + self._agent = agent + self._redirectLimit = redirectLimit +- sensitive = set(_canonicalHeaderName(each) for each in sensitiveHeaderNames) ++ sensitive = {_canonicalHeaderName(each) for each in sensitiveHeaderNames} + sensitive.update(_defaultSensitiveHeaders) + self._sensitiveHeaderNames = sensitive + +--- a/src/twisted/web/test/test_agent.py ++++ b/src/twisted/web/test/test_agent.py +@@ -4,8 +4,20 @@ + """ + Tests for L{twisted.web.client.Agent} and related new client APIs. + """ +- ++import zlib + from io import BytesIO ++from unittest import SkipTest, skipIf ++ ++from zope.interface.declarations import implementer ++from zope.interface.verify import verifyObject ++ ++from twisted.trial.unittest import TestCase, SynchronousTestCase ++from twisted.web import client, error, http_headers ++from twisted.web._newclient import RequestNotSent, RequestTransmissionFailed ++from twisted.web._newclient import ResponseNeverReceived, ResponseFailed ++from twisted.web._newclient import PotentialDataLoss ++from twisted.internet import defer, task ++from twisted.python.failure import Failure + from twisted.test.iosim import FakeTransport, IOPump + from twisted.test.proto_helpers import ( + AccumulatingProtocol, +@@ -14,16 +27,7 @@ + StringTransport, + ) + from twisted.test.test_sslverify import certificatesForAuthorityAndServer +-from unittest import skipIf, SkipTest + +-import zlib +-from twisted.trial.unittest import TestCase, SynchronousTestCase +-from twisted.web import client, error, http_headers +-from twisted.web._newclient import RequestNotSent, RequestTransmissionFailed +-from twisted.web._newclient import ResponseNeverReceived, ResponseFailed +-from twisted.web._newclient import PotentialDataLoss +-from twisted.internet import defer, task +-from twisted.python.failure import Failure + from twisted.python.compat import cookielib, intToBytes + from twisted.python.components import proxyForInterface + from twisted.internet.task import Clock +@@ -35,22 +39,22 @@ + from twisted.internet.address import IPv4Address, IPv6Address + + from twisted.web.client import ( ++ URI, + BrowserLikePolicyForHTTPS, + FileBodyProducer, + HTTPConnectionPool, + Request, + ResponseDone, +- URI, + _HTTP11ClientFactory, + ) + from twisted.web.iweb import ( ++ UNKNOWN_LENGTH, + IAgent, + IAgentEndpointFactory, + IBodyProducer, + IPolicyForHTTPS, + IRequest, + IResponse, +- UNKNOWN_LENGTH, + ) + + from twisted.web.http_headers import Headers +@@ -67,8 +71,6 @@ + ) + from twisted.web.error import SchemeNotSupported + from twisted.logger import globalLogPublisher +-from zope.interface.declarations import implementer +-from zope.interface.verify import verifyObject + + + try: +@@ -2701,15 +2703,13 @@ + targetScheme = b"https" if startScheme == b"http" else b"http" + targetPort = 443 if startPort == 80 else 80 + +- portSyntax = b'' ++ portSyntax = b"" + if crossPort: + targetPort = 8443 +- portSyntax = b':8443' ++ portSyntax = b":8443" + targetDomain = b"example.net" if crossDomain else startDomain + locationValue = targetScheme + b"://" + targetDomain + portSyntax + b"/bar" +- headers = http_headers.Headers( +- {b"location": [locationValue]} +- ) ++ headers = http_headers.Headers({b"location": [locationValue]}) + response = Response((b'HTTP', 1, 1), code, b'OK', headers, None) + res.callback(response) + +@@ -2753,7 +2753,9 @@ + self._testRedirectDefault(307) + + +- def _sensitiveHeadersTest(self, expectedHostHeader = b"example.com", **crossKwargs): ++ def _sensitiveHeadersTest( ++ self, expectedHostHeader = b"example.com", **crossKwargs ++ ): + """ + L{client.RedirectAgent} scrubs sensitive headers when redirecting + between differing origins. +@@ -2801,14 +2803,16 @@ + L{client.RedirectAgent} scrubs sensitive headers when redirecting + between differing domains. + """ +- self._sensitiveHeadersTest(crossDomain=True, expectedHostHeader=b'example.net') ++ self._sensitiveHeadersTest(crossDomain=True, expectedHostHeader=b"example.net") + + def test_crossPortHeaders(self): + """ + L{client.RedirectAgent} scrubs sensitive headers when redirecting + between differing ports. + """ +- self._sensitiveHeadersTest(crossPort=True, expectedHostHeader=b'example.com:8443') ++ self._sensitiveHeadersTest( ++ crossPort=True, expectedHostHeader=b"example.com:8443" ++ ) + + def test_crossSchemeHeaders(self): + """ diff -Nru twisted-18.9.0/debian/patches/CVE-2022-21712-6.patch twisted-18.9.0/debian/patches/CVE-2022-21712-6.patch --- twisted-18.9.0/debian/patches/CVE-2022-21712-6.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2022-21712-6.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,20 @@ +From 8a4b1f3e0e7d2316944064960d9dcb4556525845 Mon Sep 17 00:00:00 2001 +From: Glyph +Date: Sun, 23 Jan 2022 14:16:48 -0800 +Subject: [PATCH] oops + +--- + src/twisted/{topfiles => newsfragments}/10294.bugfix | 0 + 1 file changed, 0 insertions(+), 0 deletions(-) + rename src/twisted/{topfiles => newsfragments}/10294.bugfix (100%) + +--- /dev/null ++++ b/src/twisted/newsfragments/10294.bugfix +@@ -0,0 +1 @@ ++twisted.web.client.RedirectAgent and twisted.web.client.BrowserLikeRedirectAgent now properly remove sensitive headers when redirecting to a different origin. +\ No newline at end of file +--- a/src/twisted/topfiles/10294.bugfix ++++ /dev/null +@@ -1 +0,0 @@ +-twisted.web.client.RedirectAgent and twisted.web.client.BrowserLikeRedirectAgent now properly remove sensitive headers when redirecting to a different origin. +\ No newline at end of file diff -Nru twisted-18.9.0/debian/patches/CVE-2022-21712-7.patch twisted-18.9.0/debian/patches/CVE-2022-21712-7.patch --- twisted-18.9.0/debian/patches/CVE-2022-21712-7.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2022-21712-7.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,142 @@ +From 9d53c0f8b38340fe5ca3fa1a7df49347c2dea18a Mon Sep 17 00:00:00 2001 +From: Glyph +Date: Sun, 23 Jan 2022 14:37:22 -0800 +Subject: [PATCH] tell mypy enough that it can actually find bugs + +--- + src/twisted/web/test/test_agent.py | 88 +++++++++--------------------- + 1 file changed, 27 insertions(+), 61 deletions(-) + +--- a/src/twisted/web/test/test_agent.py ++++ b/src/twisted/web/test/test_agent.py +@@ -4,60 +4,28 @@ + """ + Tests for L{twisted.web.client.Agent} and related new client APIs. + """ +-import zlib + from io import BytesIO ++from twisted.test.iosim import FakeTransport, IOPump ++from twisted.test.proto_helpers import AccumulatingProtocol, EventLoggingObserver, MemoryReactorClock, StringTransport ++from twisted.test.test_sslverify import certificatesForAuthorityAndServer + from unittest import SkipTest, skipIf + +-from zope.interface.declarations import implementer +-from zope.interface.verify import verifyObject +- ++import zlib + from twisted.trial.unittest import TestCase, SynchronousTestCase + from twisted.web import client, error, http_headers +-from twisted.web._newclient import RequestNotSent, RequestTransmissionFailed +-from twisted.web._newclient import ResponseNeverReceived, ResponseFailed +-from twisted.web._newclient import PotentialDataLoss + from twisted.internet import defer, task + from twisted.python.failure import Failure +-from twisted.test.iosim import FakeTransport, IOPump +-from twisted.test.proto_helpers import ( +- AccumulatingProtocol, +- EventLoggingObserver, +- MemoryReactorClock, +- StringTransport, +-) +-from twisted.test.test_sslverify import certificatesForAuthorityAndServer + + from twisted.python.compat import cookielib, intToBytes + from twisted.python.components import proxyForInterface + from twisted.internet.task import Clock +-from twisted.internet.error import ConnectionRefusedError, ConnectionDone +-from twisted.internet.error import ConnectionLost ++from twisted.internet.error import ConnectionDone, ConnectionLost, ConnectionRefusedError + from twisted.internet.protocol import Protocol, Factory + from twisted.internet.defer import Deferred, succeed, CancelledError + from twisted.internet.endpoints import TCP4ClientEndpoint + from twisted.internet.address import IPv4Address, IPv6Address + +-from twisted.web.client import ( +- URI, +- BrowserLikePolicyForHTTPS, +- FileBodyProducer, +- HTTPConnectionPool, +- Request, +- ResponseDone, +- _HTTP11ClientFactory, +-) +-from twisted.web.iweb import ( +- UNKNOWN_LENGTH, +- IAgent, +- IAgentEndpointFactory, +- IBodyProducer, +- IPolicyForHTTPS, +- IRequest, +- IResponse, +-) +- + from twisted.web.http_headers import Headers +-from twisted.web._newclient import HTTP11ClientProtocol, Response + + from twisted.internet.interfaces import IOpenSSLClientConnectionCreator + from twisted.python.deprecate import getDeprecationWarningString +@@ -68,9 +36,26 @@ + MethodInjectionTestsMixin, + URIInjectionTestsMixin, + ) ++ ++from twisted.web._newclient import HTTP11ClientProtocol, PotentialDataLoss, RequestNotSent, RequestTransmissionFailed, Response, ResponseFailed, ResponseNeverReceived ++from twisted.web.client import BrowserLikePolicyForHTTPS, FileBodyProducer, HTTPConnectionPool, Request, ResponseDone, URI, _HTTP11ClientFactory + from twisted.web.error import SchemeNotSupported + from twisted.logger import globalLogPublisher + ++from twisted.web.iweb import IAgent, IAgentEndpointFactory, IBodyProducer, IPolicyForHTTPS, IRequest, IResponse, UNKNOWN_LENGTH ++from twisted.web.test.injectionhelpers import MethodInjectionTestsMixin, URIInjectionTestsMixin ++from zope.interface.declarations import implementer ++from zope.interface.verify import verifyObject ++ ++# Creatively lie to mypy about the nature of inheritance, since dealing with ++# expectations of a mixin class is basically impossible (don't use mixins). ++if False: # TYPE_CHECKING ++ testMixinClass = TestCase ++ runtimeTestCase = object ++else: ++ testMixinClass = object ++ runtimeTestCase = TestCase ++ + + try: + from twisted.internet import ssl +@@ -2629,11 +2614,6 @@ + b"www-authenticate", + ] + +-if False: # TYPE_CHECKING +- testMixinClass = TestCase +-else: +- testMixinClass = object +- + + class _RedirectAgentTestsMixin(testMixinClass): + """ +@@ -3035,8 +3015,9 @@ + + + +-class RedirectAgentTests(TestCase, FakeReactorAndConnectMixin, +- _RedirectAgentTestsMixin, AgentTestsMixin): ++class RedirectAgentTests( ++ FakeReactorAndConnectMixin, _RedirectAgentTestsMixin, AgentTestsMixin, runtimeTestCase, ++): + """ + Tests for L{client.RedirectAgent}. + """ +@@ -3074,10 +3055,9 @@ + + + +-class BrowserLikeRedirectAgentTests(TestCase, +- FakeReactorAndConnectMixin, +- _RedirectAgentTestsMixin, +- AgentTestsMixin): ++class BrowserLikeRedirectAgentTests( ++ FakeReactorAndConnectMixin, _RedirectAgentTestsMixin, AgentTestsMixin, runtimeTestCase ++): + """ + Tests for L{client.BrowserLikeRedirectAgent}. + """ diff -Nru twisted-18.9.0/debian/patches/CVE-2022-21712-8.patch twisted-18.9.0/debian/patches/CVE-2022-21712-8.patch --- twisted-18.9.0/debian/patches/CVE-2022-21712-8.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2022-21712-8.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,124 @@ +From adadd2c6a49a9b5f9b41a1f125004b68449d2fb5 Mon Sep 17 00:00:00 2001 +From: Glyph +Date: Sun, 23 Jan 2022 14:40:20 -0800 +Subject: [PATCH] really need to fix my editor to agree with our black / isort + config + +--- + src/twisted/web/test/test_agent.py | 72 ++++++++++++++++++++++++------ + 1 file changed, 58 insertions(+), 14 deletions(-) + +--- a/src/twisted/web/test/test_agent.py ++++ b/src/twisted/web/test/test_agent.py +@@ -4,22 +4,34 @@ + """ + Tests for L{twisted.web.client.Agent} and related new client APIs. + """ ++import zlib + from io import BytesIO +-from twisted.test.iosim import FakeTransport, IOPump +-from twisted.test.proto_helpers import AccumulatingProtocol, EventLoggingObserver, MemoryReactorClock, StringTransport +-from twisted.test.test_sslverify import certificatesForAuthorityAndServer + from unittest import SkipTest, skipIf + +-import zlib ++from zope.interface.declarations import implementer ++from zope.interface.verify import verifyObject ++ + from twisted.trial.unittest import TestCase, SynchronousTestCase + from twisted.web import client, error, http_headers + from twisted.internet import defer, task + from twisted.python.failure import Failure ++from twisted.test.iosim import FakeTransport, IOPump ++from twisted.test.proto_helpers import ( ++ AccumulatingProtocol, ++ EventLoggingObserver, ++ MemoryReactorClock, ++ StringTransport, ++) ++from twisted.test.test_sslverify import certificatesForAuthorityAndServer + + from twisted.python.compat import cookielib, intToBytes + from twisted.python.components import proxyForInterface + from twisted.internet.task import Clock +-from twisted.internet.error import ConnectionDone, ConnectionLost, ConnectionRefusedError ++from twisted.internet.error import ( ++ ConnectionDone, ++ ConnectionLost, ++ ConnectionRefusedError, ++) + from twisted.internet.protocol import Protocol, Factory + from twisted.internet.defer import Deferred, succeed, CancelledError + from twisted.internet.endpoints import TCP4ClientEndpoint +@@ -37,15 +49,41 @@ + URIInjectionTestsMixin, + ) + +-from twisted.web._newclient import HTTP11ClientProtocol, PotentialDataLoss, RequestNotSent, RequestTransmissionFailed, Response, ResponseFailed, ResponseNeverReceived +-from twisted.web.client import BrowserLikePolicyForHTTPS, FileBodyProducer, HTTPConnectionPool, Request, ResponseDone, URI, _HTTP11ClientFactory ++from twisted.web._newclient import ( ++ HTTP11ClientProtocol, ++ PotentialDataLoss, ++ RequestNotSent, ++ RequestTransmissionFailed, ++ Response, ++ ResponseFailed, ++ ResponseNeverReceived, ++) ++from twisted.web.client import ( ++ URI, ++ BrowserLikePolicyForHTTPS, ++ FileBodyProducer, ++ HTTPConnectionPool, ++ Request, ++ ResponseDone, ++ _HTTP11ClientFactory, ++) ++ + from twisted.web.error import SchemeNotSupported + from twisted.logger import globalLogPublisher + +-from twisted.web.iweb import IAgent, IAgentEndpointFactory, IBodyProducer, IPolicyForHTTPS, IRequest, IResponse, UNKNOWN_LENGTH +-from twisted.web.test.injectionhelpers import MethodInjectionTestsMixin, URIInjectionTestsMixin +-from zope.interface.declarations import implementer +-from zope.interface.verify import verifyObject ++from twisted.web.iweb import ( ++ UNKNOWN_LENGTH, ++ IAgent, ++ IAgentEndpointFactory, ++ IBodyProducer, ++ IPolicyForHTTPS, ++ IRequest, ++ IResponse, ++) ++from twisted.web.test.injectionhelpers import ( ++ MethodInjectionTestsMixin, ++ URIInjectionTestsMixin, ++) + + # Creatively lie to mypy about the nature of inheritance, since dealing with + # expectations of a mixin class is basically impossible (don't use mixins). +@@ -3016,7 +3054,10 @@ + + + class RedirectAgentTests( +- FakeReactorAndConnectMixin, _RedirectAgentTestsMixin, AgentTestsMixin, runtimeTestCase, ++ FakeReactorAndConnectMixin, ++ _RedirectAgentTestsMixin, ++ AgentTestsMixin, ++ runtimeTestCase, + ): + """ + Tests for L{client.RedirectAgent}. +@@ -3056,7 +3097,10 @@ + + + class BrowserLikeRedirectAgentTests( +- FakeReactorAndConnectMixin, _RedirectAgentTestsMixin, AgentTestsMixin, runtimeTestCase ++ FakeReactorAndConnectMixin, ++ _RedirectAgentTestsMixin, ++ AgentTestsMixin, ++ runtimeTestCase, + ): + """ + Tests for L{client.BrowserLikeRedirectAgent}. diff -Nru twisted-18.9.0/debian/patches/CVE-2022-21712-9.patch twisted-18.9.0/debian/patches/CVE-2022-21712-9.patch --- twisted-18.9.0/debian/patches/CVE-2022-21712-9.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2022-21712-9.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,19 @@ +From 4eda9eabbba8ba2ccc45daad7208ff3db25ac348 Mon Sep 17 00:00:00 2001 +From: Glyph +Date: Sun, 23 Jan 2022 14:55:10 -0800 +Subject: [PATCH] lint fix + +--- + src/twisted/web/test/test_agent.py | 3 +-- + 1 file changed, 1 insertion(+), 2 deletions(-) + +--- a/src/twisted/web/test/test_agent.py ++++ b/src/twisted/web/test/test_agent.py +@@ -77,7 +77,6 @@ + IAgentEndpointFactory, + IBodyProducer, + IPolicyForHTTPS, +- IRequest, + IResponse, + ) + from twisted.web.test.injectionhelpers import ( diff -Nru twisted-18.9.0/debian/patches/CVE-2022-21716-1.patch twisted-18.9.0/debian/patches/CVE-2022-21716-1.patch --- twisted-18.9.0/debian/patches/CVE-2022-21716-1.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2022-21716-1.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,66 @@ +From de90dfe1519e996dd150de751c670f8e03daa089 Mon Sep 17 00:00:00 2001 +From: Adi Roiban +Date: Mon, 24 Jan 2022 19:09:04 +0000 +Subject: [PATCH] Initial fix for Twisted version string DoS. + +--- + src/twisted/conch/ssh/transport.py | 9 +++++++++ + src/twisted/conch/test/test_transport.py | 22 ++++++++++++++++++++++ + src/twisted/newsfragments/10284.bugfix | 2 ++ + 3 files changed, 33 insertions(+) + create mode 100644 src/twisted/newsfragments/10284.bugfix + +--- a/src/twisted/conch/ssh/transport.py ++++ b/src/twisted/conch/ssh/transport.py +@@ -724,6 +724,15 @@ class SSHTransportBase(protocol.Protocol + """ + self.buf = self.buf + data + if not self.gotVersion: ++ ++ if len(self.buf) > 4096: ++ self.sendDisconnect( ++ DISCONNECT_CONNECTION_LOST, ++ b"Peer version string longer than 4KB. " ++ b"Preventing a deny of service attack.", ++ ) ++ return ++ + if self.buf.find(b'\n', self.buf.find(b'SSH-')) == -1: + return + +--- a/src/twisted/conch/test/test_transport.py ++++ b/src/twisted/conch/test/test_transport.py +@@ -480,6 +480,28 @@ class BaseSSHTransportTests(BaseSSHTrans + self.assertRegex(softwareVersion, softwareVersionRegex) + + ++ def test_dataReceiveVersionNotSentMemoryDOS(self): ++ """ ++ When the peer is not sending its SSH version but keeps sending data, ++ the connection is disconnected after 4KB to prevent buffering to ++ much and running our of memory. ++ """ ++ sut = MockTransportBase() ++ sut.makeConnection(self.transport) ++ ++ sut.dataReceived(b"SSH-bla-bla-bla") ++ ++ sut.dataReceived(b"more-bla-bla-bla" * 100) ++ ++ self.assertFalse(self.transport.disconnecting) ++ ++ sut.dataReceived(b"more-bla-bla-bla" * 1000) ++ ++ # Once a lot of data is received without an SSH version string, ++ # the transport is disconnected. ++ self.assertTrue(self.transport.disconnecting) ++ self.assertIn(b"Preventing a deny of service attack", self.transport.value()) ++ + def test_sendPacketPlain(self): + """ + Test that plain (unencrypted, uncompressed) packets are sent +--- /dev/null ++++ b/src/twisted/newsfragments/10284.bugfix +@@ -0,0 +1,2 @@ ++twisted.conch.ssh.transport.SSHTransportBase now disconnects the remote peer if the ++SSH version string is not sent in the first 4096 bytes. diff -Nru twisted-18.9.0/debian/patches/CVE-2022-21716-2.patch twisted-18.9.0/debian/patches/CVE-2022-21716-2.patch --- twisted-18.9.0/debian/patches/CVE-2022-21716-2.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2022-21716-2.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,49 @@ +From 9b98116372f5d211ddb9f68916d4ae73bf3c8da7 Mon Sep 17 00:00:00 2001 +From: Adi Roiban +Date: Mon, 24 Jan 2022 23:53:54 +0000 +Subject: [PATCH] Update after review. + +--- + src/twisted/conch/ssh/transport.py | 2 +- + src/twisted/conch/test/test_transport.py | 14 +++++++------- + 2 files changed, 8 insertions(+), 8 deletions(-) + +--- a/src/twisted/conch/ssh/transport.py ++++ b/src/twisted/conch/ssh/transport.py +@@ -729,7 +729,7 @@ class SSHTransportBase(protocol.Protocol + self.sendDisconnect( + DISCONNECT_CONNECTION_LOST, + b"Peer version string longer than 4KB. " +- b"Preventing a deny of service attack.", ++ b"Preventing a denial of service attack.", + ) + return + +--- a/src/twisted/conch/test/test_transport.py ++++ b/src/twisted/conch/test/test_transport.py +@@ -489,18 +489,18 @@ class BaseSSHTransportTests(BaseSSHTrans + sut = MockTransportBase() + sut.makeConnection(self.transport) + +- sut.dataReceived(b"SSH-bla-bla-bla") +- +- sut.dataReceived(b"more-bla-bla-bla" * 100) +- ++ # Data can be received over multiple chunks. ++ sut.dataReceived(b"SSH-2-Server-Identifier") ++ sut.dataReceived(b"1234567890" * 406) ++ sut.dataReceived(b"1235678") + self.assertFalse(self.transport.disconnecting) + +- sut.dataReceived(b"more-bla-bla-bla" * 1000) +- ++ # Here we are going over the limit. ++ sut.dataReceived(b"1234567") + # Once a lot of data is received without an SSH version string, + # the transport is disconnected. + self.assertTrue(self.transport.disconnecting) +- self.assertIn(b"Preventing a deny of service attack", self.transport.value()) ++ self.assertIn(b"Preventing a denial of service attack", self.transport.value()) + + def test_sendPacketPlain(self): + """ diff -Nru twisted-18.9.0/debian/patches/CVE-2022-21716-3.patch twisted-18.9.0/debian/patches/CVE-2022-21716-3.patch --- twisted-18.9.0/debian/patches/CVE-2022-21716-3.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2022-21716-3.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,20 @@ +From a4523b444760f07e609636264a61a2a07ca0bde5 Mon Sep 17 00:00:00 2001 +From: Adi Roiban +Date: Tue, 8 Feb 2022 14:01:10 +0000 +Subject: [PATCH] Fix typo. + +--- + src/twisted/conch/test/test_transport.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +--- a/src/twisted/conch/test/test_transport.py ++++ b/src/twisted/conch/test/test_transport.py +@@ -483,7 +483,7 @@ class BaseSSHTransportTests(BaseSSHTrans + def test_dataReceiveVersionNotSentMemoryDOS(self): + """ + When the peer is not sending its SSH version but keeps sending data, +- the connection is disconnected after 4KB to prevent buffering to ++ the connection is disconnected after 4KB to prevent buffering too + much and running our of memory. + """ + sut = MockTransportBase() diff -Nru twisted-18.9.0/debian/patches/CVE-2022-24801-1.patch twisted-18.9.0/debian/patches/CVE-2022-24801-1.patch --- twisted-18.9.0/debian/patches/CVE-2022-24801-1.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2022-24801-1.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,129 @@ +From: Tom Most +Date: Sat, 5 Mar 2022 23:26:55 -0800 +Subject: Some tests for GHSA-c2jg-hw38-jrqq + +--- + src/twisted/web/test/test_http.py | 103 ++++++++++++++++++++++++++++++++++++++ + 1 file changed, 103 insertions(+) + +diff --git a/src/twisted/web/test/test_http.py b/src/twisted/web/test/test_http.py +index f2a8b3e..0334d32 100644 +--- a/src/twisted/web/test/test_http.py ++++ b/src/twisted/web/test/test_http.py +@@ -1503,6 +1503,57 @@ class ParsingTests(unittest.TestCase): + request.requestHeaders.getRawHeaders(b'bAz'), [b'Quux', b'quux']) + + ++ 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 +@@ -2054,6 +2105,58 @@ Hello, + ]) + + ++ 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 diff -Nru twisted-18.9.0/debian/patches/CVE-2022-24801-2.patch twisted-18.9.0/debian/patches/CVE-2022-24801-2.patch --- twisted-18.9.0/debian/patches/CVE-2022-24801-2.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2022-24801-2.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,21 @@ +From: Tom Most +Date: Mon, 7 Mar 2022 00:02:55 -0800 +Subject: Replace obs-fold with a single space + +--- + 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 8326139..a4f1491 100644 +--- a/src/twisted/web/http.py ++++ b/src/twisted/web/http.py +@@ -2101,7 +2101,7 @@ class HTTPChannel(basic.LineReceiver, policies.TimeoutMixin): + self.setRawMode() + elif line[0] in b' \t': + # Continuation of a multi line header. +- self.__header = self.__header + '\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 -Nru twisted-18.9.0/debian/patches/CVE-2022-24801-3.patch twisted-18.9.0/debian/patches/CVE-2022-24801-3.patch --- twisted-18.9.0/debian/patches/CVE-2022-24801-3.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2022-24801-3.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,21 @@ +From: Tom Most +Date: Mon, 7 Mar 2022 00:03:50 -0800 +Subject: 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 a4f1491..e3ca5fa 100644 +--- a/src/twisted/web/http.py ++++ b/src/twisted/web/http.py +@@ -2180,7 +2180,7 @@ class HTTPChannel(basic.LineReceiver, policies.TimeoutMixin): + return False + + header = header.lower() +- data = data.strip() ++ data = data.strip(b" \t") + + if not self._maybeChooseTransferDecoder(header, data): + return False diff -Nru twisted-18.9.0/debian/patches/CVE-2022-24801-4.patch twisted-18.9.0/debian/patches/CVE-2022-24801-4.patch --- twisted-18.9.0/debian/patches/CVE-2022-24801-4.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2022-24801-4.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,21 @@ +From: Tom Most +Date: Mon, 7 Mar 2022 00:32:14 -0800 +Subject: 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 e3ca5fa..4febd1e 100644 +--- a/src/twisted/web/http.py ++++ b/src/twisted/web/http.py +@@ -2129,6 +2129,8 @@ class HTTPChannel(basic.LineReceiver, policies.TimeoutMixin): + + # Can this header determine the length? + if header == b'content-length': ++ if not data.isdigit(): ++ return fail() + try: + length = int(data) + except ValueError: diff -Nru twisted-18.9.0/debian/patches/CVE-2022-24801-5.patch twisted-18.9.0/debian/patches/CVE-2022-24801-5.patch --- twisted-18.9.0/debian/patches/CVE-2022-24801-5.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2022-24801-5.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,69 @@ +From: Tom Most +Date: Sun, 13 Mar 2022 23:19:39 -0700 +Subject: Test for malformed chunk size and extensions + +--- + src/twisted/web/test/test_http.py | 36 ++++++++++++++++++++++++++++++++++++ + 1 file changed, 36 insertions(+) + +diff --git a/src/twisted/web/test/test_http.py b/src/twisted/web/test/test_http.py +index 0334d32..155d694 100644 +--- a/src/twisted/web/test/test_http.py ++++ b/src/twisted/web/test/test_http.py +@@ -1230,6 +1230,23 @@ class ChunkedTransferEncodingTests(unittest.TestCase): + 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_finish(self): + """ + L{_ChunkedTransferDecoder.dataReceived} interprets a zero-length +@@ -1307,6 +1324,23 @@ class ChunkedTransferEncodingTests(unittest.TestCase): + self.assertEqual(successes, [True]) + + ++ 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" ++ ) ++ ++ + + class ChunkingTests(unittest.TestCase, ResponseTestMixin): + +@@ -1318,6 +1352,8 @@ class ChunkingTests(unittest.TestCase, ResponseTestMixin): + 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]) diff -Nru twisted-18.9.0/debian/patches/CVE-2022-24801-6.patch twisted-18.9.0/debian/patches/CVE-2022-24801-6.patch --- twisted-18.9.0/debian/patches/CVE-2022-24801-6.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2022-24801-6.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,112 @@ +From: Tom Most +Date: Sun, 13 Mar 2022 23:51:52 -0700 +Subject: Reject malformed chunk sizes + +--- + src/twisted/web/http.py | 30 +++++++++++++++++++++++++++-- + src/twisted/web/test/test_http.py | 40 +++++++++++++++++++++++++++++++++++++++ + 2 files changed, 68 insertions(+), 2 deletions(-) + +diff --git a/src/twisted/web/http.py b/src/twisted/web/http.py +index 4febd1e..e2f74a6 100644 +--- a/src/twisted/web/http.py ++++ b/src/twisted/web/http.py +@@ -338,6 +338,32 @@ def toChunk(data): + + + ++def _ishexdigits(b): ++ """ ++ 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): ++ """ ++ 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): + """ + Convert chunk to string. +@@ -350,7 +376,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': +@@ -1771,7 +1797,7 @@ class _ChunkedTransferDecoder(object): + line, rest = data.split(b'\r\n', 1) + parts = line.split(b';') + try: +- self.length = int(parts[0], 16) ++ self.length = _hexint(parts[0]) + 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 155d694..5161acb 100644 +--- a/src/twisted/web/test/test_http.py ++++ b/src/twisted/web/test/test_http.py +@@ -4095,3 +4095,43 @@ class ChannelProductionTests(unittest.TestCase): + clock.advance(1) + self.assertIs(transport.producer, None) + self.assertIs(transport.streaming, None) ++ ++ ++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) diff -Nru twisted-18.9.0/debian/patches/CVE-2022-24801-7.patch twisted-18.9.0/debian/patches/CVE-2022-24801-7.patch --- twisted-18.9.0/debian/patches/CVE-2022-24801-7.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2022-24801-7.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,22 @@ +From: Tom Most +Date: Sun, 13 Mar 2022 23:55:26 -0700 +Subject: 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 e2f74a6..99641ff 100644 +--- a/src/twisted/web/http.py ++++ b/src/twisted/web/http.py +@@ -368,6 +368,9 @@ def fromChunk(data): + """ + 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}. diff -Nru twisted-18.9.0/debian/patches/CVE-2022-24801-8.patch twisted-18.9.0/debian/patches/CVE-2022-24801-8.patch --- twisted-18.9.0/debian/patches/CVE-2022-24801-8.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2022-24801-8.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,100 @@ +From: Tom Most +Date: Sun, 27 Mar 2022 22:17:30 -0700 +Subject: Correct chunk extension byte validation + +Go back to the RFC to figure out the correct allowed ranges. +--- + src/twisted/web/http.py | 46 ++++++++++++++++++++++++++++++++++++++- + src/twisted/web/test/test_http.py | 8 ++++++- + 2 files changed, 52 insertions(+), 2 deletions(-) + +diff --git a/src/twisted/web/http.py b/src/twisted/web/http.py +index 99641ff..c992deb 100644 +--- a/src/twisted/web/http.py ++++ b/src/twisted/web/http.py +@@ -346,7 +346,7 @@ def _ishexdigits(b): + and 0-9. + """ + for c in b: +- if c not in b'0123456789abcdefABCDEF': ++ if c not in b"0123456789abcdefABCDEF": + return False + return bool(b) + +@@ -1748,6 +1748,46 @@ class _IdentityTransferDecoder(object): + raise _DataLoss() + + ++_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(object): + """ +@@ -1804,6 +1844,10 @@ class _ChunkedTransferDecoder(object): + except ValueError: + raise _MalformedChunkedDataError( + "Chunk-size must be an integer.") ++ if len(parts) > 1 and parts[1].translate(None, _chunkExtChars) != b"": ++ raise _MalformedChunkedDataError( ++ "Invalid characters in chunk extensions: %r." % parts[1] ++ ) + if self.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 5161acb..21d2713 100644 +--- a/src/twisted/web/test/test_http.py ++++ b/src/twisted/web/test/test_http.py +@@ -1238,7 +1238,13 @@ class ChunkedTransferEncodingTests(unittest.TestCase): + + 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 diff -Nru twisted-18.9.0/debian/patches/CVE-2022-24801-9.patch twisted-18.9.0/debian/patches/CVE-2022-24801-9.patch --- twisted-18.9.0/debian/patches/CVE-2022-24801-9.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/CVE-2022-24801-9.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,38 @@ +From: Tom Most +Date: Fri, 1 Apr 2022 20:47:59 -0700 +Subject: 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 c992deb..1a4e7cb 100644 +--- a/src/twisted/web/http.py ++++ b/src/twisted/web/http.py +@@ -348,7 +348,7 @@ def _ishexdigits(b): + for c in b: + if c not in b"0123456789abcdefABCDEF": + return False +- return bool(b) ++ return b != b"" + + + def _hexint(b): +@@ -1764,14 +1764,14 @@ _chunkExtChars = ( + """ + 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 + diff -Nru twisted-18.9.0/debian/patches/Tests-remove-spurious-test-for-illegal-whitespace-in-xmlns.patch twisted-18.9.0/debian/patches/Tests-remove-spurious-test-for-illegal-whitespace-in-xmlns.patch --- twisted-18.9.0/debian/patches/Tests-remove-spurious-test-for-illegal-whitespace-in-xmlns.patch 1970-01-01 00:00:00.000000000 +0000 +++ twisted-18.9.0/debian/patches/Tests-remove-spurious-test-for-illegal-whitespace-in-xmlns.patch 2022-05-05 14:01:06.000000000 +0000 @@ -0,0 +1,37 @@ +From: Glyph +Date: Sun, 27 Mar 2022 18:03:49 -0700 +Subject: remove spurious test for illegal whitespace in xmlns + +Origin: upstream, https://github.com/twisted/twisted/commit/6b4bbf9040abd8e5c8feae026c4a6483c7f72506 +--- + src/twisted/newsfragments/10318.misc | 0 + src/twisted/words/test/test_domish.py | 12 ------------ + 2 files changed, 12 deletions(-) + create mode 100644 src/twisted/newsfragments/10318.misc + +diff --git a/src/twisted/newsfragments/10318.misc b/src/twisted/newsfragments/10318.misc +new file mode 100644 +index 0000000..e69de29 +diff --git a/src/twisted/words/test/test_domish.py b/src/twisted/words/test/test_domish.py +index a8f8fa7..cecf61c 100644 +--- a/src/twisted/words/test/test_domish.py ++++ b/src/twisted/words/test/test_domish.py +@@ -338,18 +338,6 @@ class DomishStreamTestsMixin: + self.assertEqual(self.elements[0].child2.uri, '') + + +- def test_namespaceWithWhitespace(self): +- """ +- Whitespace in an xmlns value is preserved in the resulting node's C{uri} +- attribute. +- """ +- xml = b"" +- self.stream.parse(xml) +- self.assertEqual(self.elements[0].uri, " bar baz ") +- self.assertEqual( +- self.elements[0].attributes, {(" bar baz ", "baz"): "quux"}) +- +- + def testChildPrefix(self): + xml = b"" + diff -Nru twisted-18.9.0/debian/patches/series twisted-18.9.0/debian/patches/series --- twisted-18.9.0/debian/patches/series 2018-12-07 10:23:30.000000000 +0000 +++ twisted-18.9.0/debian/patches/series 2022-05-05 14:01:06.000000000 +0000 @@ -13,3 +13,45 @@ 0013-Drop-test_givesMeaningfulErrorMessageIfNoCipherMatch.patch 0014-OpenSSL-may-not-use-ECDH-by-default-thus-drop-this-t.patch 0015-Fix-tests-to-expect-new-web-request-logging-format.patch +CVE-2019-12387.patch +CVE-2019-12855-01.patch +CVE-2019-12855-02.patch +CVE-2019-12855-03.patch +CVE-2019-12855-04.patch +CVE-2019-12855-05.patch +CVE-2019-12855-06.patch +CVE-2019-12855-07.patch +CVE-2019-12855-09.patch +CVE-2019-12855-10.patch +CVE-2019-12855-11.patch +CVE-2019-12855-12.patch +CVE-2019-12855-13.patch +CVE-2019-12855-14.patch +CVE-2019-12855-15.patch +CVE-2019-12855-17.patch +CVE-2019-951x.patch +CVE-2020-1010x-pre1.patch +CVE-2020-1010x.patch +CVE-2022-21712-1.patch +CVE-2022-21712-2.patch +CVE-2022-21712-3.patch +CVE-2022-21712-4.patch +CVE-2022-21712-5.patch +CVE-2022-21712-6.patch +CVE-2022-21712-7.patch +CVE-2022-21712-8.patch +CVE-2022-21712-9.patch +CVE-2022-21712-10.patch +CVE-2022-21716-1.patch +CVE-2022-21716-2.patch +CVE-2022-21716-3.patch +CVE-2022-24801-1.patch +CVE-2022-24801-2.patch +CVE-2022-24801-3.patch +CVE-2022-24801-4.patch +CVE-2022-24801-5.patch +CVE-2022-24801-6.patch +CVE-2022-24801-7.patch +CVE-2022-24801-8.patch +CVE-2022-24801-9.patch +Tests-remove-spurious-test-for-illegal-whitespace-in-xmlns.patch