Compare commits
10 Commits
94b2ade490
...
f5140b9581
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5140b9581 | ||
|
|
1122c37bc4 | ||
|
|
e9fae94081 | ||
|
|
be2d80c9e5 | ||
|
|
d8228895e8 | ||
|
|
3d4d520225 | ||
|
|
bdc65240c3 | ||
|
|
872194c206 | ||
|
|
b801533a3d | ||
|
|
685eb2d175 |
@ -1,907 +0,0 @@
|
|||||||
diff --git a/src/twisted/web/_newclient.py b/src/twisted/web/_newclient.py
|
|
||||||
index 370f47d..74a8a6c 100644
|
|
||||||
--- a/src/twisted/web/_newclient.py
|
|
||||||
+++ b/src/twisted/web/_newclient.py
|
|
||||||
@@ -29,6 +29,8 @@ Various other classes in this module support this usage:
|
|
||||||
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:
|
|
||||||
diff --git a/src/twisted/web/client.py b/src/twisted/web/client.py
|
|
||||||
index 02eb6e9..a1554d3 100644
|
|
||||||
--- a/src/twisted/web/client.py
|
|
||||||
+++ b/src/twisted/web/client.py
|
|
||||||
@@ -46,6 +46,9 @@ from twisted.web.iweb import UNKNOWN_LENGTH, IAgent, IBodyProducer, IResponse
|
|
||||||
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.ClientFactory):
|
|
||||||
# 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.ClientFactory):
|
|
||||||
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, factoryFactory, contextFactory=None,
|
|
||||||
|
|
||||||
@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)
|
|
||||||
diff --git a/src/twisted/web/newsfragments/9647.bugfix b/src/twisted/web/newsfragments/9647.bugfix
|
|
||||||
new file mode 100644
|
|
||||||
index 0000000..b76916c
|
|
||||||
--- /dev/null
|
|
||||||
+++ b/src/twisted/web/newsfragments/9647.bugfix
|
|
||||||
@@ -0,0 +1 @@
|
|
||||||
+All HTTP clients in twisted.web.client now raise a ValueError when called with a method and/or URL that contain invalid characters. This mitigates CVE-2019-12387. Thanks to Alex Brasetvik for reporting this vulnerability.
|
|
||||||
\ No newline at end of file
|
|
||||||
diff --git a/src/twisted/web/test/injectionhelpers.py b/src/twisted/web/test/injectionhelpers.py
|
|
||||||
new file mode 100644
|
|
||||||
index 0000000..ffeb862
|
|
||||||
--- /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")
|
|
||||||
diff --git a/src/twisted/web/test/test_agent.py b/src/twisted/web/test/test_agent.py
|
|
||||||
index 7a7669b..9b57512 100644
|
|
||||||
--- 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 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
|
|
||||||
|
|
||||||
@@ -886,6 +890,7 @@ class AgentTests(TestCase, FakeReactorAndConnectMixin, AgentTestsMixin,
|
|
||||||
"""
|
|
||||||
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, FakeReactorAndConnectMixin, AgentTestsMixin,
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
+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,100 @@ 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())
|
|
||||||
diff --git a/src/twisted/web/test/test_webclient.py b/src/twisted/web/test/test_webclient.py
|
|
||||||
index 41cff54..680e027 100644
|
|
||||||
--- 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} APIs, C{getPage} and friends.
|
|
||||||
|
|
||||||
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)
|
|
||||||
1307
CVE-2019-12855.patch
1307
CVE-2019-12855.patch
File diff suppressed because it is too large
Load Diff
@ -1,260 +0,0 @@
|
|||||||
From 4a7d22e490bb8ff836892cc99a1f54b85ccb0281 Mon Sep 17 00:00:00 2001
|
|
||||||
From: Mark Williams <mrw@enotuniq.org>
|
|
||||||
Date: Sun, 16 Feb 2020 19:00:10 -0800
|
|
||||||
Subject: [PATCH] Fix several request smuggling attacks.
|
|
||||||
|
|
||||||
1. Requests with multiple Content-Length headers were allowed (thanks
|
|
||||||
to Jake Miller from Bishop Fox and ZeddYu Lu) and now fail with a 400;
|
|
||||||
|
|
||||||
2. Requests with a Content-Length header and a Transfer-Encoding
|
|
||||||
header honored the first header (thanks to Jake Miller from Bishop
|
|
||||||
Fox) and now fail with a 400;
|
|
||||||
|
|
||||||
3. Requests whose Transfer-Encoding header had a value other than
|
|
||||||
"chunked" and "identity" (thanks to ZeddYu Lu) were allowed and now fail
|
|
||||||
with a 400.
|
|
||||||
---
|
|
||||||
src/twisted/web/http.py | 64 +++++++---
|
|
||||||
src/twisted/web/newsfragments/9770.bugfix | 1 +
|
|
||||||
src/twisted/web/test/test_http.py | 137 ++++++++++++++++++++++
|
|
||||||
3 files changed, 187 insertions(+), 15 deletions(-)
|
|
||||||
create mode 100644 src/twisted/web/newsfragments/9770.bugfix
|
|
||||||
|
|
||||||
diff --git a/src/twisted/web/http.py b/src/twisted/web/http.py
|
|
||||||
index f0fb05b4d69..06d830fe30f 100644
|
|
||||||
--- a/src/twisted/web/http.py
|
|
||||||
+++ b/src/twisted/web/http.py
|
|
||||||
@@ -2171,6 +2171,51 @@ def _finishRequestBody(self, data):
|
|
||||||
self.allContentReceived()
|
|
||||||
self._dataBuffer.append(data)
|
|
||||||
|
|
||||||
+ def _maybeChooseTransferDecoder(self, header, data):
|
|
||||||
+ """
|
|
||||||
+ If the provided header is C{content-length} or
|
|
||||||
+ C{transfer-encoding}, choose the appropriate decoder if any.
|
|
||||||
+
|
|
||||||
+ Returns L{True} if the request can proceed and L{False} if not.
|
|
||||||
+ """
|
|
||||||
+
|
|
||||||
+ def fail():
|
|
||||||
+ self._respondToBadRequestAndDisconnect()
|
|
||||||
+ self.length = None
|
|
||||||
+
|
|
||||||
+ # Can this header determine the length?
|
|
||||||
+ if header == b'content-length':
|
|
||||||
+ try:
|
|
||||||
+ length = int(data)
|
|
||||||
+ except ValueError:
|
|
||||||
+ fail()
|
|
||||||
+ return False
|
|
||||||
+ newTransferDecoder = _IdentityTransferDecoder(
|
|
||||||
+ length, self.requests[-1].handleContentChunk, self._finishRequestBody)
|
|
||||||
+ elif header == b'transfer-encoding':
|
|
||||||
+ # XXX Rather poorly tested code block, apparently only exercised by
|
|
||||||
+ # test_chunkedEncoding
|
|
||||||
+ if data.lower() == b'chunked':
|
|
||||||
+ length = None
|
|
||||||
+ newTransferDecoder = _ChunkedTransferDecoder(
|
|
||||||
+ self.requests[-1].handleContentChunk, self._finishRequestBody)
|
|
||||||
+ elif data.lower() == b'identity':
|
|
||||||
+ return True
|
|
||||||
+ else:
|
|
||||||
+ fail()
|
|
||||||
+ return False
|
|
||||||
+ else:
|
|
||||||
+ # It's not a length related header, so exit
|
|
||||||
+ return True
|
|
||||||
+
|
|
||||||
+ if self._transferDecoder is not None:
|
|
||||||
+ fail()
|
|
||||||
+ return False
|
|
||||||
+ else:
|
|
||||||
+ self.length = length
|
|
||||||
+ self._transferDecoder = newTransferDecoder
|
|
||||||
+ return True
|
|
||||||
+
|
|
||||||
|
|
||||||
def headerReceived(self, line):
|
|
||||||
"""
|
|
||||||
@@ -2196,21 +2241,10 @@ def headerReceived(self, line):
|
|
||||||
|
|
||||||
header = header.lower()
|
|
||||||
data = data.strip()
|
|
||||||
- if header == b'content-length':
|
|
||||||
- try:
|
|
||||||
- self.length = int(data)
|
|
||||||
- except ValueError:
|
|
||||||
- self._respondToBadRequestAndDisconnect()
|
|
||||||
- self.length = None
|
|
||||||
- return False
|
|
||||||
- self._transferDecoder = _IdentityTransferDecoder(
|
|
||||||
- self.length, self.requests[-1].handleContentChunk, self._finishRequestBody)
|
|
||||||
- elif header == b'transfer-encoding' and data.lower() == b'chunked':
|
|
||||||
- # XXX Rather poorly tested code block, apparently only exercised by
|
|
||||||
- # test_chunkedEncoding
|
|
||||||
- self.length = None
|
|
||||||
- self._transferDecoder = _ChunkedTransferDecoder(
|
|
||||||
- self.requests[-1].handleContentChunk, self._finishRequestBody)
|
|
||||||
+
|
|
||||||
+ if not self._maybeChooseTransferDecoder(header, data):
|
|
||||||
+ return False
|
|
||||||
+
|
|
||||||
reqHeaders = self.requests[-1].requestHeaders
|
|
||||||
values = reqHeaders.getRawHeaders(header)
|
|
||||||
if values is not None:
|
|
||||||
diff --git a/src/twisted/web/newsfragments/9770.bugfix b/src/twisted/web/newsfragments/9770.bugfix
|
|
||||||
new file mode 100644
|
|
||||||
index 00000000000..4f1be97de8a
|
|
||||||
--- /dev/null
|
|
||||||
+++ b/src/twisted/web/newsfragments/9770.bugfix
|
|
||||||
@@ -0,0 +1 @@
|
|
||||||
+Fix several request smuggling attacks: requests with multiple Content-Length headers were allowed (thanks to Jake Miller from Bishop Fox and ZeddYu Lu) and now fail with a 400; requests with a Content-Length header and a Transfer-Encoding header honored the first header (thanks to Jake Miller from Bishop Fox) and now fail with a 400; requests whose Transfer-Encoding header had a value other than "chunked" and "identity" (thanks to ZeddYu Lu) were allowed and now fail a 400.
|
|
||||||
\ No newline at end of file
|
|
||||||
diff --git a/src/twisted/web/test/test_http.py b/src/twisted/web/test/test_http.py
|
|
||||||
index 0a0db09b750..578cb500cda 100644
|
|
||||||
--- a/src/twisted/web/test/test_http.py
|
|
||||||
+++ b/src/twisted/web/test/test_http.py
|
|
||||||
@@ -2252,6 +2252,143 @@ def process(self):
|
|
||||||
self.flushLoggedErrors(AttributeError)
|
|
||||||
|
|
||||||
|
|
||||||
+ def assertDisconnectingBadRequest(self, request):
|
|
||||||
+ """
|
|
||||||
+ Assert that the given request bytes fail with a 400 bad
|
|
||||||
+ request without calling L{Request.process}.
|
|
||||||
+
|
|
||||||
+ @param request: A raw HTTP request
|
|
||||||
+ @type request: L{bytes}
|
|
||||||
+ """
|
|
||||||
+ class FailedRequest(http.Request):
|
|
||||||
+ processed = False
|
|
||||||
+ def process(self):
|
|
||||||
+ FailedRequest.processed = True
|
|
||||||
+
|
|
||||||
+ channel = self.runRequest(request, FailedRequest, success=False)
|
|
||||||
+ self.assertFalse(FailedRequest.processed, "Request.process called")
|
|
||||||
+ self.assertEqual(
|
|
||||||
+ channel.transport.value(),
|
|
||||||
+ b"HTTP/1.1 400 Bad Request\r\n\r\n")
|
|
||||||
+ self.assertTrue(channel.transport.disconnecting)
|
|
||||||
+
|
|
||||||
+
|
|
||||||
+ def test_duplicateContentLengths(self):
|
|
||||||
+ """
|
|
||||||
+ A request which includes multiple C{content-length} headers
|
|
||||||
+ fails with a 400 response without calling L{Request.process}.
|
|
||||||
+ """
|
|
||||||
+ self.assertRequestRejected([
|
|
||||||
+ b'GET /a HTTP/1.1',
|
|
||||||
+ b'Content-Length: 56',
|
|
||||||
+ b'Content-Length: 0',
|
|
||||||
+ b'Host: host.invalid',
|
|
||||||
+ b'',
|
|
||||||
+ b'',
|
|
||||||
+ ])
|
|
||||||
+
|
|
||||||
+
|
|
||||||
+ def test_duplicateContentLengthsWithPipelinedRequests(self):
|
|
||||||
+ """
|
|
||||||
+ Two pipelined requests, the first of which includes multiple
|
|
||||||
+ C{content-length} headers, trigger a 400 response without
|
|
||||||
+ calling L{Request.process}.
|
|
||||||
+ """
|
|
||||||
+ self.assertRequestRejected([
|
|
||||||
+ b'GET /a HTTP/1.1',
|
|
||||||
+ b'Content-Length: 56',
|
|
||||||
+ b'Content-Length: 0',
|
|
||||||
+ b'Host: host.invalid',
|
|
||||||
+ b'',
|
|
||||||
+ b'',
|
|
||||||
+ b'GET /a HTTP/1.1',
|
|
||||||
+ b'Host: host.invalid',
|
|
||||||
+ b'',
|
|
||||||
+ b'',
|
|
||||||
+ ])
|
|
||||||
+
|
|
||||||
+
|
|
||||||
+ def test_contentLengthAndTransferEncoding(self):
|
|
||||||
+ """
|
|
||||||
+ A request that includes both C{content-length} and
|
|
||||||
+ C{transfer-encoding} headers fails with a 400 response without
|
|
||||||
+ calling L{Request.process}.
|
|
||||||
+ """
|
|
||||||
+ self.assertRequestRejected([
|
|
||||||
+ b'GET /a HTTP/1.1',
|
|
||||||
+ b'Transfer-Encoding: chunked',
|
|
||||||
+ b'Content-Length: 0',
|
|
||||||
+ b'Host: host.invalid',
|
|
||||||
+ b'',
|
|
||||||
+ b'',
|
|
||||||
+ ])
|
|
||||||
+
|
|
||||||
+
|
|
||||||
+ def test_contentLengthAndTransferEncodingWithPipelinedRequests(self):
|
|
||||||
+ """
|
|
||||||
+ Two pipelined requests, the first of which includes both
|
|
||||||
+ C{content-length} and C{transfer-encoding} headers, triggers a
|
|
||||||
+ 400 response without calling L{Request.process}.
|
|
||||||
+ """
|
|
||||||
+ self.assertRequestRejected([
|
|
||||||
+ b'GET /a HTTP/1.1',
|
|
||||||
+ b'Transfer-Encoding: chunked',
|
|
||||||
+ b'Content-Length: 0',
|
|
||||||
+ b'Host: host.invalid',
|
|
||||||
+ b'',
|
|
||||||
+ b'',
|
|
||||||
+ b'GET /a HTTP/1.1',
|
|
||||||
+ b'Host: host.invalid',
|
|
||||||
+ b'',
|
|
||||||
+ b'',
|
|
||||||
+ ])
|
|
||||||
+
|
|
||||||
+
|
|
||||||
+ def test_unknownTransferEncoding(self):
|
|
||||||
+ """
|
|
||||||
+ A request whose C{transfer-encoding} header includes a value
|
|
||||||
+ other than C{chunked} or C{identity} fails with a 400 response
|
|
||||||
+ without calling L{Request.process}.
|
|
||||||
+ """
|
|
||||||
+ self.assertRequestRejected([
|
|
||||||
+ b'GET /a HTTP/1.1',
|
|
||||||
+ b'Transfer-Encoding: unknown',
|
|
||||||
+ b'Host: host.invalid',
|
|
||||||
+ b'',
|
|
||||||
+ b'',
|
|
||||||
+ ])
|
|
||||||
+
|
|
||||||
+
|
|
||||||
+ def test_transferEncodingIdentity(self):
|
|
||||||
+ """
|
|
||||||
+ A request with a valid C{content-length} and a
|
|
||||||
+ C{transfer-encoding} whose value is C{identity} succeeds.
|
|
||||||
+ """
|
|
||||||
+ body = []
|
|
||||||
+
|
|
||||||
+ class SuccessfulRequest(http.Request):
|
|
||||||
+ processed = False
|
|
||||||
+ def process(self):
|
|
||||||
+ body.append(self.content.read())
|
|
||||||
+ self.setHeader(b'content-length', b'0')
|
|
||||||
+ self.finish()
|
|
||||||
+
|
|
||||||
+ request = b'''\
|
|
||||||
+GET / HTTP/1.1
|
|
||||||
+Host: host.invalid
|
|
||||||
+Content-Length: 2
|
|
||||||
+Transfer-Encoding: identity
|
|
||||||
+
|
|
||||||
+ok
|
|
||||||
+'''
|
|
||||||
+ channel = self.runRequest(request, SuccessfulRequest, False)
|
|
||||||
+ self.assertEqual(body, [b'ok'])
|
|
||||||
+ self.assertEqual(
|
|
||||||
+ channel.transport.value(),
|
|
||||||
+ b'HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n',
|
|
||||||
+ )
|
|
||||||
+
|
|
||||||
+
|
|
||||||
|
|
||||||
class QueryArgumentsTests(unittest.TestCase):
|
|
||||||
def testParseqs(self):
|
|
||||||
192
CVE-2023-46137.patch
Normal file
192
CVE-2023-46137.patch
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
From 8d500550fdee4c55e3158f8d8c293c2dc1587869 Mon Sep 17 00:00:00 2001
|
||||||
|
From: starlet-dx <15929766099@163.com>
|
||||||
|
Date: Fri, 29 Dec 2023 15:36:52 +0800
|
||||||
|
Subject: [PATCH 1/1] 11976 stop processing pipelined HTTP/1.1 requests that are received together #11979
|
||||||
|
|
||||||
|
Origin:
|
||||||
|
https://github.com/twisted/twisted/commit/1e6e9d23cac59689760558dcb6634285e694b04c
|
||||||
|
---
|
||||||
|
src/twisted/web/http.py | 32 +++++++--
|
||||||
|
src/twisted/web/newsfragments/11976.bugfix | 7 ++
|
||||||
|
src/twisted/web/test/test_web.py | 81 +++++++++++++++++++++-
|
||||||
|
3 files changed, 114 insertions(+), 6 deletions(-)
|
||||||
|
create mode 100644 src/twisted/web/newsfragments/11976.bugfix
|
||||||
|
|
||||||
|
diff --git a/src/twisted/web/http.py b/src/twisted/web/http.py
|
||||||
|
index b80a55a..23f8817 100644
|
||||||
|
--- a/src/twisted/web/http.py
|
||||||
|
+++ b/src/twisted/web/http.py
|
||||||
|
@@ -2443,14 +2443,38 @@ class HTTPChannel(basic.LineReceiver, policies.TimeoutMixin):
|
||||||
|
|
||||||
|
self._handlingRequest = True
|
||||||
|
|
||||||
|
+ # We go into raw mode here even though we will be receiving lines next
|
||||||
|
+ # in the protocol; however, this data will be buffered and then passed
|
||||||
|
+ # back to line mode in the setLineMode call in requestDone.
|
||||||
|
+ self.setRawMode()
|
||||||
|
+
|
||||||
|
req = self.requests[-1]
|
||||||
|
req.requestReceived(command, path, version)
|
||||||
|
|
||||||
|
- def dataReceived(self, data):
|
||||||
|
+ def rawDataReceived(self, data: bytes) -> None:
|
||||||
|
"""
|
||||||
|
- Data was received from the network. Process it.
|
||||||
|
+ This is called when this HTTP/1.1 parser is in raw mode rather than
|
||||||
|
+ line mode.
|
||||||
|
+
|
||||||
|
+ It may be in raw mode for one of two reasons:
|
||||||
|
+
|
||||||
|
+ 1. All the headers of a request have been received and this
|
||||||
|
+ L{HTTPChannel} is currently receiving its body.
|
||||||
|
+
|
||||||
|
+ 2. The full content of a request has been received and is currently
|
||||||
|
+ being processed asynchronously, and this L{HTTPChannel} is
|
||||||
|
+ buffering the data of all subsequent requests to be parsed
|
||||||
|
+ later.
|
||||||
|
+
|
||||||
|
+ In the second state, the data will be played back later.
|
||||||
|
+
|
||||||
|
+ @note: This isn't really a public API, and should be invoked only by
|
||||||
|
+ L{LineReceiver}'s line parsing logic. If you wish to drive an
|
||||||
|
+ L{HTTPChannel} from a custom data source, call C{dataReceived} on
|
||||||
|
+ it directly.
|
||||||
|
+
|
||||||
|
+ @see: L{LineReceive.rawDataReceived}
|
||||||
|
"""
|
||||||
|
- # If we're currently handling a request, buffer this data.
|
||||||
|
if self._handlingRequest:
|
||||||
|
self._dataBuffer.append(data)
|
||||||
|
if (
|
||||||
|
@@ -2462,9 +2486,7 @@ class HTTPChannel(basic.LineReceiver, policies.TimeoutMixin):
|
||||||
|
# ready. See docstring for _optimisticEagerReadSize above.
|
||||||
|
self._networkProducer.pauseProducing()
|
||||||
|
return
|
||||||
|
- return basic.LineReceiver.dataReceived(self, data)
|
||||||
|
|
||||||
|
- def rawDataReceived(self, data):
|
||||||
|
self.resetTimeout()
|
||||||
|
|
||||||
|
try:
|
||||||
|
diff --git a/src/twisted/web/newsfragments/11976.bugfix b/src/twisted/web/newsfragments/11976.bugfix
|
||||||
|
new file mode 100644
|
||||||
|
index 0000000..8ac292b
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/src/twisted/web/newsfragments/11976.bugfix
|
||||||
|
@@ -0,0 +1,7 @@
|
||||||
|
+In Twisted 16.3.0, we changed twisted.web to stop dispatching HTTP/1.1
|
||||||
|
+pipelined requests to application code. There was a bug in this change which
|
||||||
|
+still allowed clients which could send multiple full HTTP requests in a single
|
||||||
|
+TCP segment to trigger asynchronous processing of later requests, which could
|
||||||
|
+lead to out-of-order responses. This has now been corrected and twisted.web
|
||||||
|
+should never process a pipelined request over HTTP/1.1 until the previous
|
||||||
|
+request has fully completed.
|
||||||
|
diff --git a/src/twisted/web/test/test_web.py b/src/twisted/web/test/test_web.py
|
||||||
|
index 3eb35a9..b2b2ad7 100644
|
||||||
|
--- a/src/twisted/web/test/test_web.py
|
||||||
|
+++ b/src/twisted/web/test/test_web.py
|
||||||
|
@@ -8,6 +8,7 @@ Tests for various parts of L{twisted.web}.
|
||||||
|
import os
|
||||||
|
import zlib
|
||||||
|
from io import BytesIO
|
||||||
|
+from typing import List
|
||||||
|
|
||||||
|
from zope.interface import implementer
|
||||||
|
from zope.interface.verify import verifyObject
|
||||||
|
@@ -17,10 +18,13 @@ from twisted.internet.address import IPv4Address, IPv6Address
|
||||||
|
from twisted.internet.task import Clock
|
||||||
|
from twisted.logger import LogLevel, globalLogPublisher
|
||||||
|
from twisted.python import failure, reflect
|
||||||
|
+from twisted.python.compat import iterbytes
|
||||||
|
from twisted.python.filepath import FilePath
|
||||||
|
-from twisted.test.proto_helpers import EventLoggingObserver
|
||||||
|
+from twisted.test.proto_helpers import EventLoggingObserver, StringTransport
|
||||||
|
from twisted.trial import unittest
|
||||||
|
from twisted.web import error, http, iweb, resource, server
|
||||||
|
+from twisted.web.resource import Resource
|
||||||
|
+from twisted.web.server import NOT_DONE_YET, Request, Site
|
||||||
|
from twisted.web.static import Data
|
||||||
|
from twisted.web.test.requesthelper import DummyChannel, DummyRequest
|
||||||
|
from ._util import assertIsFilesystemTemporary
|
||||||
|
@@ -1849,3 +1853,78 @@ class ExplicitHTTPFactoryReactor(unittest.TestCase):
|
||||||
|
|
||||||
|
factory = http.HTTPFactory()
|
||||||
|
self.assertIs(factory.reactor, reactor)
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+class QueueResource(Resource):
|
||||||
|
+ """
|
||||||
|
+ Add all requests to an internal queue,
|
||||||
|
+ without responding to the requests.
|
||||||
|
+ You can access the requests from the queue and handle their response.
|
||||||
|
+ """
|
||||||
|
+
|
||||||
|
+ isLeaf = True
|
||||||
|
+
|
||||||
|
+ def __init__(self) -> None:
|
||||||
|
+ super().__init__()
|
||||||
|
+ self.dispatchedRequests: List[Request] = []
|
||||||
|
+
|
||||||
|
+ def render_GET(self, request: Request) -> int:
|
||||||
|
+ self.dispatchedRequests.append(request)
|
||||||
|
+ return NOT_DONE_YET
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+class TestRFC9112Section932(unittest.TestCase):
|
||||||
|
+ """
|
||||||
|
+ Verify that HTTP/1.1 request ordering is preserved.
|
||||||
|
+ """
|
||||||
|
+
|
||||||
|
+ def test_multipleRequestsInOneSegment(self) -> None:
|
||||||
|
+ """
|
||||||
|
+ Twisted MUST NOT respond to a second HTTP/1.1 request while the first
|
||||||
|
+ is still pending.
|
||||||
|
+ """
|
||||||
|
+ qr = QueueResource()
|
||||||
|
+ site = Site(qr)
|
||||||
|
+ proto = site.buildProtocol(None)
|
||||||
|
+ serverTransport = StringTransport()
|
||||||
|
+ proto.makeConnection(serverTransport)
|
||||||
|
+ proto.dataReceived(
|
||||||
|
+ b"GET /first HTTP/1.1\r\nHost: a\r\n\r\n"
|
||||||
|
+ b"GET /second HTTP/1.1\r\nHost: a\r\n\r\n"
|
||||||
|
+ )
|
||||||
|
+ # The TCP data contains 2 requests,
|
||||||
|
+ # but only 1 request was dispatched,
|
||||||
|
+ # as the first request was not yet finalized.
|
||||||
|
+ self.assertEqual(len(qr.dispatchedRequests), 1)
|
||||||
|
+ # The first request is finalized and the
|
||||||
|
+ # second request is dispatched right away.
|
||||||
|
+ qr.dispatchedRequests[0].finish()
|
||||||
|
+ self.assertEqual(len(qr.dispatchedRequests), 2)
|
||||||
|
+
|
||||||
|
+ def test_multipleRequestsInDifferentSegments(self) -> None:
|
||||||
|
+ """
|
||||||
|
+ Twisted MUST NOT respond to a second HTTP/1.1 request while the first
|
||||||
|
+ is still pending, even if the second request is received in a separate
|
||||||
|
+ TCP package.
|
||||||
|
+ """
|
||||||
|
+ qr = QueueResource()
|
||||||
|
+ site = Site(qr)
|
||||||
|
+ proto = site.buildProtocol(None)
|
||||||
|
+ serverTransport = StringTransport()
|
||||||
|
+ proto.makeConnection(serverTransport)
|
||||||
|
+ raw_data = (
|
||||||
|
+ b"GET /first HTTP/1.1\r\nHost: a\r\n\r\n"
|
||||||
|
+ b"GET /second HTTP/1.1\r\nHost: a\r\n\r\n"
|
||||||
|
+ )
|
||||||
|
+ # Just go byte by byte for the extreme case in which each byte is
|
||||||
|
+ # received in a separate TCP package.
|
||||||
|
+ for chunk in iterbytes(raw_data):
|
||||||
|
+ proto.dataReceived(chunk)
|
||||||
|
+ # The TCP data contains 2 requests,
|
||||||
|
+ # but only 1 request was dispatched,
|
||||||
|
+ # as the first request was not yet finalized.
|
||||||
|
+ self.assertEqual(len(qr.dispatchedRequests), 1)
|
||||||
|
+ # The first request is finalized and the
|
||||||
|
+ # second request is dispatched right away.
|
||||||
|
+ qr.dispatchedRequests[0].finish()
|
||||||
|
+ self.assertEqual(len(qr.dispatchedRequests), 2)
|
||||||
|
--
|
||||||
|
2.30.0
|
||||||
|
|
||||||
322
CVE-2024-41671.patch
Normal file
322
CVE-2024-41671.patch
Normal file
@ -0,0 +1,322 @@
|
|||||||
|
From ef2c755e9e9d57d58132af790bd2fd2b957b3fb1 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Tom Most <twm@freecog.net>
|
||||||
|
Date: Mon, 22 Jul 2024 23:21:49 -0700
|
||||||
|
Subject: [PATCH] Tests and partial fix
|
||||||
|
|
||||||
|
---
|
||||||
|
src/twisted/web/http.py | 35 +++-
|
||||||
|
src/twisted/web/newsfragments/12248.bugfix | 1 +
|
||||||
|
src/twisted/web/test/test_http.py | 191 +++++++++++++++++++--
|
||||||
|
3 files changed, 212 insertions(+), 15 deletions(-)
|
||||||
|
create mode 100644 src/twisted/web/newsfragments/12248.bugfix
|
||||||
|
|
||||||
|
diff --git a/src/twisted/web/http.py b/src/twisted/web/http.py
|
||||||
|
index 23f8817..c7216d0 100644
|
||||||
|
--- a/src/twisted/web/http.py
|
||||||
|
+++ b/src/twisted/web/http.py
|
||||||
|
@@ -1921,6 +1921,9 @@ class _ChunkedTransferDecoder:
|
||||||
|
self.finishCallback = finishCallback
|
||||||
|
self._buffer = bytearray()
|
||||||
|
self._start = 0
|
||||||
|
+ self._trailerHeaders = []
|
||||||
|
+ self._maxTrailerHeadersSize = 2**16
|
||||||
|
+ self._receivedTrailerHeadersSize = 0
|
||||||
|
|
||||||
|
def _dataReceived_CHUNK_LENGTH(self) -> bool:
|
||||||
|
"""
|
||||||
|
@@ -2007,11 +2010,35 @@ class _ChunkedTransferDecoder:
|
||||||
|
@raises _MalformedChunkedDataError: when anything other than CRLF is
|
||||||
|
received.
|
||||||
|
"""
|
||||||
|
- if len(self._buffer) < 2:
|
||||||
|
+ eolIndex = self._buffer.find(b"\r\n", self._start)
|
||||||
|
+
|
||||||
|
+ if eolIndex == -1:
|
||||||
|
+ # Still no end of network line marker found.
|
||||||
|
+ #
|
||||||
|
+ # Check if we've run up against the trailer size limit: if the next
|
||||||
|
+ # read contains the terminating CRLF then we'll have this many bytes
|
||||||
|
+ # of trailers (including the CRLFs).
|
||||||
|
+ minTrailerSize = (
|
||||||
|
+ self._receivedTrailerHeadersSize
|
||||||
|
+ + len(self._buffer)
|
||||||
|
+ + (1 if self._buffer.endswith(b"\r") else 2)
|
||||||
|
+ )
|
||||||
|
+ if minTrailerSize > self._maxTrailerHeadersSize:
|
||||||
|
+ raise _MalformedChunkedDataError("Trailer headers data is too long.")
|
||||||
|
+ # Continue processing more data.
|
||||||
|
return False
|
||||||
|
|
||||||
|
- if not self._buffer.startswith(b"\r\n"):
|
||||||
|
- raise _MalformedChunkedDataError("Chunk did not end with CRLF")
|
||||||
|
+ if eolIndex > 0:
|
||||||
|
+ # A trailer header was detected.
|
||||||
|
+ self._trailerHeaders.append(self._buffer[0:eolIndex])
|
||||||
|
+ del self._buffer[0 : eolIndex + 2]
|
||||||
|
+ self._start = 0
|
||||||
|
+ self._receivedTrailerHeadersSize += eolIndex + 2
|
||||||
|
+ if self._receivedTrailerHeadersSize > self._maxTrailerHeadersSize:
|
||||||
|
+ raise _MalformedChunkedDataError("Trailer headers data is too long.")
|
||||||
|
+ return True
|
||||||
|
+
|
||||||
|
+ # eolIndex in this part of code is equal to 0
|
||||||
|
|
||||||
|
data = memoryview(self._buffer)[2:].tobytes()
|
||||||
|
del self._buffer[:]
|
||||||
|
@@ -2331,8 +2358,8 @@ class HTTPChannel(basic.LineReceiver, policies.TimeoutMixin):
|
||||||
|
self.__header = line
|
||||||
|
|
||||||
|
def _finishRequestBody(self, data):
|
||||||
|
- self.allContentReceived()
|
||||||
|
self._dataBuffer.append(data)
|
||||||
|
+ self.allContentReceived()
|
||||||
|
|
||||||
|
def _maybeChooseTransferDecoder(self, header, data):
|
||||||
|
"""
|
||||||
|
diff --git a/src/twisted/web/newsfragments/12248.bugfix b/src/twisted/web/newsfragments/12248.bugfix
|
||||||
|
new file mode 100644
|
||||||
|
index 0000000..2fb6067
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/src/twisted/web/newsfragments/12248.bugfix
|
||||||
|
@@ -0,0 +1 @@
|
||||||
|
+The HTTP 1.0 and 1.1 server provided by twisted.web could process pipelined HTTP requests out-of-order, possibly resulting in information disclosure (CVE-2024-41671/GHSA-c8m8-j448-xjx7)
|
||||||
|
diff --git a/src/twisted/web/test/test_http.py b/src/twisted/web/test/test_http.py
|
||||||
|
index f304991..ae061cd 100644
|
||||||
|
--- a/src/twisted/web/test/test_http.py
|
||||||
|
+++ b/src/twisted/web/test/test_http.py
|
||||||
|
@@ -460,10 +460,9 @@ class HTTP1_0Tests(unittest.TestCase, ResponseTestMixin):
|
||||||
|
# one byte at a time, to stress it.
|
||||||
|
for byte in iterbytes(self.requests):
|
||||||
|
a.dataReceived(byte)
|
||||||
|
- value = b.value()
|
||||||
|
|
||||||
|
# So far only one request should have been dispatched.
|
||||||
|
- self.assertEqual(value, b"")
|
||||||
|
+ self.assertEqual(b.value(), b"")
|
||||||
|
self.assertEqual(1, len(a.requests))
|
||||||
|
|
||||||
|
# Now, process each request one at a time.
|
||||||
|
@@ -472,8 +471,95 @@ class HTTP1_0Tests(unittest.TestCase, ResponseTestMixin):
|
||||||
|
request = a.requests[0].original
|
||||||
|
request.delayedProcess()
|
||||||
|
|
||||||
|
- value = b.value()
|
||||||
|
- self.assertResponseEquals(value, self.expected_response)
|
||||||
|
+ self.assertResponseEquals(b.value(), self.expectedResponses)
|
||||||
|
+
|
||||||
|
+ def test_stepwiseDumpTruck(self):
|
||||||
|
+ """
|
||||||
|
+ Imitate a fast connection where several pipelined
|
||||||
|
+ requests arrive in a single read. The request handler
|
||||||
|
+ (L{DelayedHTTPHandler}) is puppeted to step through the
|
||||||
|
+ handling of each request.
|
||||||
|
+ """
|
||||||
|
+ b = StringTransport()
|
||||||
|
+ a = http.HTTPChannel()
|
||||||
|
+ a.requestFactory = DelayedHTTPHandlerProxy
|
||||||
|
+ a.makeConnection(b)
|
||||||
|
+
|
||||||
|
+ a.dataReceived(self.requests)
|
||||||
|
+
|
||||||
|
+ # So far only one request should have been dispatched.
|
||||||
|
+ self.assertEqual(b.value(), b"")
|
||||||
|
+ self.assertEqual(1, len(a.requests))
|
||||||
|
+
|
||||||
|
+ # Now, process each request one at a time.
|
||||||
|
+ while a.requests:
|
||||||
|
+ self.assertEqual(1, len(a.requests))
|
||||||
|
+ request = a.requests[0].original
|
||||||
|
+ request.delayedProcess()
|
||||||
|
+
|
||||||
|
+ self.assertResponseEquals(b.value(), self.expectedResponses)
|
||||||
|
+
|
||||||
|
+ def test_immediateTinyTube(self):
|
||||||
|
+ """
|
||||||
|
+ Imitate a slow connection that delivers one byte at a time.
|
||||||
|
+
|
||||||
|
+ (L{DummyHTTPHandler}) immediately responds, but no more
|
||||||
|
+ than one
|
||||||
|
+ """
|
||||||
|
+ b = StringTransport()
|
||||||
|
+ a = http.HTTPChannel()
|
||||||
|
+ a.requestFactory = DummyHTTPHandlerProxy # "sync"
|
||||||
|
+ a.makeConnection(b)
|
||||||
|
+
|
||||||
|
+ # one byte at a time, to stress it.
|
||||||
|
+ for byte in iterbytes(self.requests):
|
||||||
|
+ a.dataReceived(byte)
|
||||||
|
+ # There is never more than one request dispatched at a time:
|
||||||
|
+ self.assertLessEqual(len(a.requests), 1)
|
||||||
|
+
|
||||||
|
+ self.assertResponseEquals(b.value(), self.expectedResponses)
|
||||||
|
+
|
||||||
|
+ def test_immediateDumpTruck(self):
|
||||||
|
+ """
|
||||||
|
+ Imitate a fast connection where several pipelined
|
||||||
|
+ requests arrive in a single read. The request handler
|
||||||
|
+ (L{DummyHTTPHandler}) immediately responds.
|
||||||
|
+
|
||||||
|
+ This doesn't check the at-most-one pending request
|
||||||
|
+ invariant but exercises otherwise uncovered code paths.
|
||||||
|
+ See GHSA-c8m8-j448-xjx7.
|
||||||
|
+ """
|
||||||
|
+ b = StringTransport()
|
||||||
|
+ a = http.HTTPChannel()
|
||||||
|
+ a.requestFactory = DummyHTTPHandlerProxy
|
||||||
|
+ a.makeConnection(b)
|
||||||
|
+
|
||||||
|
+ # All bytes at once to ensure there's stuff to buffer.
|
||||||
|
+ a.dataReceived(self.requests)
|
||||||
|
+
|
||||||
|
+ self.assertResponseEquals(b.value(), self.expectedResponses)
|
||||||
|
+
|
||||||
|
+ def test_immediateABiggerTruck(self):
|
||||||
|
+ """
|
||||||
|
+ Imitate a fast connection where a so many pipelined
|
||||||
|
+ requests arrive in a single read that backpressure is indicated.
|
||||||
|
+ The request handler (L{DummyHTTPHandler}) immediately responds.
|
||||||
|
+
|
||||||
|
+ This doesn't check the at-most-one pending request
|
||||||
|
+ invariant but exercises otherwise uncovered code paths.
|
||||||
|
+ See GHSA-c8m8-j448-xjx7.
|
||||||
|
+
|
||||||
|
+ @see: L{http.HTTPChannel._optimisticEagerReadSize}
|
||||||
|
+ """
|
||||||
|
+ b = StringTransport()
|
||||||
|
+ a = http.HTTPChannel()
|
||||||
|
+ a.requestFactory = DummyHTTPHandlerProxy
|
||||||
|
+ a.makeConnection(b)
|
||||||
|
+
|
||||||
|
+ overLimitCount = a._optimisticEagerReadSize // len(self.requests) * 10
|
||||||
|
+ a.dataReceived(self.requests * overLimitCount)
|
||||||
|
+
|
||||||
|
+ self.assertResponseEquals(b.value(), self.expectedResponses * overLimitCount)
|
||||||
|
|
||||||
|
|
||||||
|
class HTTP1_1Tests(HTTP1_0Tests):
|
||||||
|
@@ -574,10 +660,15 @@ class PipeliningBodyTests(unittest.TestCase, ResponseTestMixin):
|
||||||
|
b"POST / HTTP/1.1\r\n"
|
||||||
|
b"Content-Length: 10\r\n"
|
||||||
|
b"\r\n"
|
||||||
|
- b"0123456789POST / HTTP/1.1\r\n"
|
||||||
|
- b"Content-Length: 10\r\n"
|
||||||
|
- b"\r\n"
|
||||||
|
b"0123456789"
|
||||||
|
+ # Chunk encoded request.
|
||||||
|
+ b"POST / HTTP/1.1\r\n"
|
||||||
|
+ b"Transfer-Encoding: chunked\r\n"
|
||||||
|
+ b"\r\n"
|
||||||
|
+ b"a\r\n"
|
||||||
|
+ b"0123456789\r\n"
|
||||||
|
+ b"0\r\n"
|
||||||
|
+ b"\r\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
expectedResponses = [
|
||||||
|
@@ -594,14 +685,16 @@ class PipeliningBodyTests(unittest.TestCase, ResponseTestMixin):
|
||||||
|
b"Request: /",
|
||||||
|
b"Command: POST",
|
||||||
|
b"Version: HTTP/1.1",
|
||||||
|
- b"Content-Length: 21",
|
||||||
|
- b"'''\n10\n0123456789'''\n",
|
||||||
|
+ b"Content-Length: 23",
|
||||||
|
+ b"'''\nNone\n0123456789'''\n",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
- def test_noPipelining(self):
|
||||||
|
+ def test_stepwiseTinyTube(self):
|
||||||
|
"""
|
||||||
|
- Test that pipelined requests get buffered, not processed in parallel.
|
||||||
|
+ Imitate a slow connection that delivers one byte at a time.
|
||||||
|
+ The request handler (L{DelayedHTTPHandler}) is puppeted to
|
||||||
|
+ step through the handling of each request.
|
||||||
|
"""
|
||||||
|
b = StringTransport()
|
||||||
|
a = http.HTTPChannel()
|
||||||
|
@@ -1474,6 +1567,82 @@ class ChunkedTransferEncodingTests(unittest.TestCase):
|
||||||
|
self.assertEqual(errors, [])
|
||||||
|
self.assertEqual(successes, [True])
|
||||||
|
|
||||||
|
+ def test_trailerHeaders(self):
|
||||||
|
+ """
|
||||||
|
+ L{_ChunkedTransferDecoder.dataReceived} decodes chunked-encoded data
|
||||||
|
+ and ignores trailer headers which come after the terminating zero-length
|
||||||
|
+ chunk.
|
||||||
|
+ """
|
||||||
|
+ L = []
|
||||||
|
+ finished = []
|
||||||
|
+ p = http._ChunkedTransferDecoder(L.append, finished.append)
|
||||||
|
+ p.dataReceived(b"3\r\nabc\r\n5\r\n12345\r\n")
|
||||||
|
+ p.dataReceived(
|
||||||
|
+ b"a\r\n0123456789\r\n0\r\nServer-Timing: total;dur=123.4\r\nExpires: Wed, 21 Oct 2015 07:28:00 GMT\r\n\r\n"
|
||||||
|
+ )
|
||||||
|
+ self.assertEqual(L, [b"abc", b"12345", b"0123456789"])
|
||||||
|
+ self.assertEqual(finished, [b""])
|
||||||
|
+ self.assertEqual(
|
||||||
|
+ p._trailerHeaders,
|
||||||
|
+ [
|
||||||
|
+ b"Server-Timing: total;dur=123.4",
|
||||||
|
+ b"Expires: Wed, 21 Oct 2015 07:28:00 GMT",
|
||||||
|
+ ],
|
||||||
|
+ )
|
||||||
|
+
|
||||||
|
+ def test_shortTrailerHeader(self):
|
||||||
|
+ """
|
||||||
|
+ L{_ChunkedTransferDecoder.dataReceived} decodes chunks of input with
|
||||||
|
+ tailer header broken up and delivered in multiple calls.
|
||||||
|
+ """
|
||||||
|
+ L = []
|
||||||
|
+ finished = []
|
||||||
|
+ p = http._ChunkedTransferDecoder(L.append, finished.append)
|
||||||
|
+ for s in iterbytes(
|
||||||
|
+ b"3\r\nabc\r\n5\r\n12345\r\n0\r\nServer-Timing: total;dur=123.4\r\n\r\n"
|
||||||
|
+ ):
|
||||||
|
+ p.dataReceived(s)
|
||||||
|
+ self.assertEqual(L, [b"a", b"b", b"c", b"1", b"2", b"3", b"4", b"5"])
|
||||||
|
+ self.assertEqual(finished, [b""])
|
||||||
|
+ self.assertEqual(p._trailerHeaders, [b"Server-Timing: total;dur=123.4"])
|
||||||
|
+
|
||||||
|
+ def test_tooLongTrailerHeader(self):
|
||||||
|
+ r"""
|
||||||
|
+ L{_ChunkedTransferDecoder.dataReceived} raises
|
||||||
|
+ L{_MalformedChunkedDataError} when the trailing headers data is too long.
|
||||||
|
+ """
|
||||||
|
+ p = http._ChunkedTransferDecoder(
|
||||||
|
+ lambda b: None,
|
||||||
|
+ lambda b: None, # pragma: nocov
|
||||||
|
+ )
|
||||||
|
+ p._maxTrailerHeadersSize = 10
|
||||||
|
+ self.assertRaises(
|
||||||
|
+ http._MalformedChunkedDataError,
|
||||||
|
+ p.dataReceived,
|
||||||
|
+ b"3\r\nabc\r\n0\r\nTotal-Trailer: header;greater-then=10\r\n\r\n",
|
||||||
|
+ )
|
||||||
|
+
|
||||||
|
+ def test_unfinishedTrailerHeader(self):
|
||||||
|
+ r"""
|
||||||
|
+ L{_ChunkedTransferDecoder.dataReceived} raises
|
||||||
|
+ L{_MalformedChunkedDataError} when the trailing headers data is too long
|
||||||
|
+ and doesn't have final CRLF characters.
|
||||||
|
+ """
|
||||||
|
+ p = http._ChunkedTransferDecoder(
|
||||||
|
+ lambda b: None,
|
||||||
|
+ lambda b: None, # pragma: nocov
|
||||||
|
+ )
|
||||||
|
+ p._maxTrailerHeadersSize = 10
|
||||||
|
+ # 9 bytes are received so far, in 2 packets.
|
||||||
|
+ # For now, all is ok.
|
||||||
|
+ p.dataReceived(b"3\r\nabc\r\n0\r\n01234567")
|
||||||
|
+ p.dataReceived(b"\r")
|
||||||
|
+ # Once the 10th byte is received, the processing fails.
|
||||||
|
+ self.assertRaises(
|
||||||
|
+ http._MalformedChunkedDataError,
|
||||||
|
+ p.dataReceived,
|
||||||
|
+ b"A",
|
||||||
|
+ )
|
||||||
|
|
||||||
|
class ChunkingTests(unittest.TestCase, ResponseTestMixin):
|
||||||
|
|
||||||
|
--
|
||||||
|
2.41.0
|
||||||
|
|
||||||
102
CVE-2024-41810.patch
Normal file
102
CVE-2024-41810.patch
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
From a22866244736345239909eaca7be2eb8da791997 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Viktor Chuchurski <viktor@doyensec.com>
|
||||||
|
Date: Thu, 25 Jul 2024 19:34:35 +0200
|
||||||
|
Subject: [PATCH 1/6] - added output encoding in redirect HTML
|
||||||
|
|
||||||
|
---
|
||||||
|
src/twisted/web/_template_util.py | 2 +-
|
||||||
|
src/twisted/web/newsfragments/12263.bugfix | 1 +
|
||||||
|
src/twisted/web/newsfragments/9839.bugfix | 1 +
|
||||||
|
src/twisted/web/test/test_util.py | 39 +++++++++++++++++++++-
|
||||||
|
4 files changed, 41 insertions(+), 2 deletions(-)
|
||||||
|
create mode 100644 src/twisted/web/newsfragments/12263.bugfix
|
||||||
|
create mode 100644 src/twisted/web/newsfragments/9839.bugfix
|
||||||
|
|
||||||
|
diff --git a/src/twisted/web/_template_util.py b/src/twisted/web/_template_util.py
|
||||||
|
index 38ebbed..c6f7e9d 100644
|
||||||
|
--- a/src/twisted/web/_template_util.py
|
||||||
|
+++ b/src/twisted/web/_template_util.py
|
||||||
|
@@ -92,7 +92,7 @@ def redirectTo(URL: bytes, request: IRequest) -> bytes:
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
""" % {
|
||||||
|
- b"url": URL
|
||||||
|
+ b"url": escape(URL.decode("utf-8")).encode("utf-8")
|
||||||
|
}
|
||||||
|
return content
|
||||||
|
|
||||||
|
diff --git a/src/twisted/web/newsfragments/12263.bugfix b/src/twisted/web/newsfragments/12263.bugfix
|
||||||
|
new file mode 100644
|
||||||
|
index 0000000..b3982ca
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/src/twisted/web/newsfragments/12263.bugfix
|
||||||
|
@@ -0,0 +1 @@
|
||||||
|
+twisted.web.util.redirectTo now HTML-escapes the provided URL in the fallback response body it returns (GHSA-cf56-g6w6-pqq2). The issue is being tracked with CVE-2024-41810.
|
||||||
|
\ No newline at end of file
|
||||||
|
diff --git a/src/twisted/web/newsfragments/9839.bugfix b/src/twisted/web/newsfragments/9839.bugfix
|
||||||
|
new file mode 100644
|
||||||
|
index 0000000..1e2e7f7
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/src/twisted/web/newsfragments/9839.bugfix
|
||||||
|
@@ -0,0 +1 @@
|
||||||
|
+twisted.web.util.redirectTo now HTML-escapes the provided URL in the fallback response body it returns (GHSA-cf56-g6w6-pqq2, CVE-2024-41810).
|
||||||
|
diff --git a/src/twisted/web/test/test_util.py b/src/twisted/web/test/test_util.py
|
||||||
|
index 996b0d0..87282ce 100644
|
||||||
|
--- a/src/twisted/web/test/test_util.py
|
||||||
|
+++ b/src/twisted/web/test/test_util.py
|
||||||
|
@@ -5,7 +5,6 @@
|
||||||
|
Tests for L{twisted.web.util}.
|
||||||
|
"""
|
||||||
|
|
||||||
|
-
|
||||||
|
import gc
|
||||||
|
|
||||||
|
from twisted.internet import defer
|
||||||
|
@@ -64,6 +63,44 @@ class RedirectToTests(TestCase):
|
||||||
|
targetURL = "http://target.example.com/4321"
|
||||||
|
self.assertRaises(TypeError, redirectTo, targetURL, request)
|
||||||
|
|
||||||
|
+ def test_legitimateRedirect(self):
|
||||||
|
+ """
|
||||||
|
+ Legitimate URLs are fully interpolated in the `redirectTo` response body without transformation
|
||||||
|
+ """
|
||||||
|
+ request = DummyRequest([b""])
|
||||||
|
+ html = redirectTo(b"https://twisted.org/", request)
|
||||||
|
+ expected = b"""
|
||||||
|
+<html>
|
||||||
|
+ <head>
|
||||||
|
+ <meta http-equiv=\"refresh\" content=\"0;URL=https://twisted.org/\">
|
||||||
|
+ </head>
|
||||||
|
+ <body bgcolor=\"#FFFFFF\" text=\"#000000\">
|
||||||
|
+ <a href=\"https://twisted.org/\">click here</a>
|
||||||
|
+ </body>
|
||||||
|
+</html>
|
||||||
|
+"""
|
||||||
|
+ self.assertEqual(html, expected)
|
||||||
|
+
|
||||||
|
+ def test_maliciousRedirect(self):
|
||||||
|
+ """
|
||||||
|
+ Malicious URLs are HTML-escaped before interpolating them in the `redirectTo` response body
|
||||||
|
+ """
|
||||||
|
+ request = DummyRequest([b""])
|
||||||
|
+ html = redirectTo(
|
||||||
|
+ b'https://twisted.org/"><script>alert(document.location)</script>', request
|
||||||
|
+ )
|
||||||
|
+ expected = b"""
|
||||||
|
+<html>
|
||||||
|
+ <head>
|
||||||
|
+ <meta http-equiv=\"refresh\" content=\"0;URL=https://twisted.org/"><script>alert(document.location)</script>\">
|
||||||
|
+ </head>
|
||||||
|
+ <body bgcolor=\"#FFFFFF\" text=\"#000000\">
|
||||||
|
+ <a href=\"https://twisted.org/"><script>alert(document.location)</script>\">click here</a>
|
||||||
|
+ </body>
|
||||||
|
+</html>
|
||||||
|
+"""
|
||||||
|
+ self.assertEqual(html, expected)
|
||||||
|
+
|
||||||
|
|
||||||
|
class ParentRedirectTests(SynchronousTestCase):
|
||||||
|
"""
|
||||||
|
--
|
||||||
|
2.41.0
|
||||||
|
|
||||||
Binary file not shown.
@ -1,15 +1,16 @@
|
|||||||
|
%define debug_package %{nil}
|
||||||
Name: python-twisted
|
Name: python-twisted
|
||||||
Version: 18.9.0
|
Version: 22.10.0
|
||||||
Release: 7
|
Release: 4
|
||||||
Summary: An event-driven networking engine written in Python
|
Summary: An event-driven networking engine written in Python
|
||||||
License: MIT
|
License: MIT
|
||||||
URL: http://twistedmatrix.com/
|
URL: http://twistedmatrix.com/
|
||||||
Source0: https://files.pythonhosted.org/packages/source/T/Twisted/Twisted-%{version}.tar.bz2
|
Source0: https://github.com/twisted/twisted/archive/twisted-%{version}/twisted-%{version}.tar.gz
|
||||||
|
# https://github.com/twisted/twisted/commit/1e6e9d23cac59689760558dcb6634285e694b04c
|
||||||
|
Patch0: CVE-2023-46137.patch
|
||||||
|
Patch1: CVE-2024-41810.patch
|
||||||
|
Patch2: CVE-2024-41671.patch
|
||||||
|
|
||||||
# https://github.com/twisted/twisted/commit/6c61fc4503ae39ab8ecee52d10f10ee2c371d7e2
|
|
||||||
Patch0000: CVE-2019-12387.patch
|
|
||||||
Patch0001: CVE-2020-10109_10108.patch
|
|
||||||
Patch0002: CVE-2019-12855.patch
|
|
||||||
|
|
||||||
%description
|
%description
|
||||||
Twisted is an event-based framework for internet applications,
|
Twisted is an event-based framework for internet applications,
|
||||||
@ -38,7 +39,7 @@ Summary: An event-driven networking engine written in Python
|
|||||||
BuildRequires: python3-devel >= 3.3 python3dist(appdirs) >= 1.4.0
|
BuildRequires: python3-devel >= 3.3 python3dist(appdirs) >= 1.4.0
|
||||||
BuildRequires: python3dist(automat) >= 0.3.0 python3dist(constantly) >= 15.1
|
BuildRequires: python3dist(automat) >= 0.3.0 python3dist(constantly) >= 15.1
|
||||||
BuildRequires: python3dist(cryptography) >= 1.5 python3-hyperlink >= 17.1.1
|
BuildRequires: python3dist(cryptography) >= 1.5 python3-hyperlink >= 17.1.1
|
||||||
BuildRequires: (python3dist(h2) >= 3.0 with python3dist(h2) < 4.0)
|
BuildRequires: (python3dist(h2) >= 3.0 with python3dist(h2) < 5.0)
|
||||||
BuildRequires: python3dist(idna) >= 0.6 python3dist(incremental) >= 16.10.1
|
BuildRequires: python3dist(idna) >= 0.6 python3dist(incremental) >= 16.10.1
|
||||||
BuildRequires: (python3dist(priority) >= 1.1.0 with python3dist(priority) < 2.0)
|
BuildRequires: (python3dist(priority) >= 1.1.0 with python3dist(priority) < 2.0)
|
||||||
BuildRequires: python3dist(pyasn1) python3dist(pyopenssl) >= 16.0.0
|
BuildRequires: python3dist(pyasn1) python3dist(pyopenssl) >= 16.0.0
|
||||||
@ -75,15 +76,11 @@ BuildArch: noarch
|
|||||||
The python-twisted-help package contains related documents.
|
The python-twisted-help package contains related documents.
|
||||||
|
|
||||||
%prep
|
%prep
|
||||||
%autosetup -n Twisted-%{version} -p1
|
%autosetup -n twisted-twisted-%{version} -p1
|
||||||
|
|
||||||
%build
|
%build
|
||||||
%py3_build
|
%py3_build
|
||||||
|
|
||||||
PYTHONPATH=${PWD}/src/ sphinx-build-3 -a docs html
|
|
||||||
rm -rf html/.doctrees
|
|
||||||
rm -rf html/.buildinfo
|
|
||||||
|
|
||||||
%install
|
%install
|
||||||
%py3_install
|
%py3_install
|
||||||
mv %{buildroot}%{_bindir}/trial %{buildroot}%{_bindir}/trial-%{python3_version}
|
mv %{buildroot}%{_bindir}/trial %{buildroot}%{_bindir}/trial-%{python3_version}
|
||||||
@ -92,29 +89,44 @@ ln -s ./trial-%{python3_version} %{buildroot}%{_bindir}/trial-3
|
|||||||
ln -s ./twistd-%{python3_version} %{buildroot}%{_bindir}/twistd-3
|
ln -s ./twistd-%{python3_version} %{buildroot}%{_bindir}/twistd-3
|
||||||
ln -s ./trial-%{python3_version} %{buildroot}%{_bindir}/trial
|
ln -s ./trial-%{python3_version} %{buildroot}%{_bindir}/trial
|
||||||
ln -s ./twistd-%{python3_version} %{buildroot}%{_bindir}/twistd
|
ln -s ./twistd-%{python3_version} %{buildroot}%{_bindir}/twistd
|
||||||
chmod +x %{buildroot}%{python3_sitearch}/twisted/mail/test/pop3testserver.py
|
chmod +x %{buildroot}%{python3_sitelib}/twisted/mail/test/pop3testserver.py
|
||||||
chmod +x %{buildroot}%{python3_sitearch}/twisted/trial/test/scripttest.py
|
chmod +x %{buildroot}%{python3_sitelib}/twisted/trial/test/scripttest.py
|
||||||
|
|
||||||
pathfix.py -pn -i %{__python3} %{buildroot}%{python3_sitearch}
|
pathfix.py -pn -i %{__python3} %{buildroot}%{python3_sitelib}
|
||||||
install -d %{buildroot}%{_mandir}/man1/
|
install -d %{buildroot}%{_mandir}/man1/
|
||||||
cp -a docs/conch/man/*.1 %{buildroot}%{_mandir}/man1/
|
cp -a docs/conch/man/*.1 %{buildroot}%{_mandir}/man1/
|
||||||
cp -a docs/core/man/*.1 %{buildroot}%{_mandir}/man1/
|
cp -a docs/core/man/*.1 %{buildroot}%{_mandir}/man1/
|
||||||
cp -a docs/mail/man/*.1 %{buildroot}%{_mandir}/man1/
|
cp -a docs/mail/man/*.1 %{buildroot}%{_mandir}/man1/
|
||||||
|
|
||||||
%check
|
%check
|
||||||
PATH=%{buildroot}%{_bindir}:$PATH PYTHONPATH=%{buildroot}%{python3_sitearch} %{buildroot}%{_bindir}/trial-3 twisted ||:
|
PATH=%{buildroot}%{_bindir}:$PATH PYTHONPATH=%{buildroot}%{python3_sitelib} %{buildroot}%{_bindir}/trial-3 twisted ||:
|
||||||
|
|
||||||
%files -n python3-twisted
|
%files -n python3-twisted
|
||||||
%doc CONTRIBUTING NEWS.rst README.rst html LICENSE
|
%doc NEWS.rst README.rst LICENSE
|
||||||
%{_bindir}/{trial-3*,twistd-3*}
|
%{_bindir}/{trial-3*,twistd-3*}
|
||||||
%{python3_sitearch}/twisted
|
%{python3_sitelib}/twisted
|
||||||
%{python3_sitearch}/Twisted-%{version}-py%{python3_version}.egg-info
|
%{python3_sitelib}/Twisted-%{version}-py%{python3_version}.egg-info
|
||||||
%{_bindir}/{cftp,ckeygen,conch,mailmail,pyhtmlizer,tkconch,trial,twist,twistd}
|
%{_bindir}/{cftp,ckeygen,conch,mailmail,pyhtmlizer,tkconch,trial,twist,twistd}
|
||||||
|
|
||||||
%files help
|
%files help
|
||||||
%{_mandir}/man1/{cftp.1*,ckeygen.1*,conch.1*,mailmail.1*,pyhtmlizer.1*,tkconch.1*,trial.1*,twistd.1*}
|
%{_mandir}/man1/{cftp.1*,ckeygen.1*,conch.1*,mailmail.1*,pyhtmlizer.1*,tkconch.1*,trial.1*,twistd.1*}
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
|
* Tue Jul 30 2024 yinyongkang <yinyongkang@kylinos.cn> - 22.10.0-4
|
||||||
|
- Fix CVE-2024-41810 and CVE-2024-41671
|
||||||
|
|
||||||
|
* Fri Dec 29 2023 yaoxin <yao_xin001@hoperun.com> - 22.10.0-3
|
||||||
|
- Fix CVE-2023-46137
|
||||||
|
|
||||||
|
* Thu Jan 19 2023 caodongxia <caodongxia@h-partners.com> - 22.10.0-2
|
||||||
|
- Modify the version restriction of python-h2
|
||||||
|
|
||||||
|
* Wed Dec 07 2022 jiangxinyu <jiangxinyu@kylinos.cn> - 22.10.0-1
|
||||||
|
- Update package to version 22.10.0
|
||||||
|
|
||||||
|
* Wed Aug 03 2022 duyiwei <duyiwei@kylinos.cn> - 22.4.0-1
|
||||||
|
- upgrade version to 22.4.0
|
||||||
|
|
||||||
* Mon May 09 2022 xu_ping <xuping33@h-partners.com> - 18.9.0-7
|
* Mon May 09 2022 xu_ping <xuping33@h-partners.com> - 18.9.0-7
|
||||||
- fix build error caused by python3.10 wildcard
|
- fix build error caused by python3.10 wildcard
|
||||||
|
|
||||||
|
|||||||
BIN
twisted-22.10.0.tar.gz
Normal file
BIN
twisted-22.10.0.tar.gz
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user