high

CVE

CVE-2026-115775, CVE-2026-115776, CVE-2026-115778, CVE-2026-115877, CVE-2026-115774, CVE-2026-83813

CWE

CWE-400, CWE-770, CWE-444, CWE-835, CWE-346

Affected Surface

  • PyPI `Twisted` (`twisted`) versions `<= 26.4.0` (`< 26.10.0rc1`)
  • Python services using `twisted.words.xish.domish` (`ExpatElementStream` or `SuxElementStream`) for XMPP or XML stream parsing (`CVE-2026-115775` / `GHSA-3476-c5c2-fr73`)
  • Python services using `twisted.words.protocols.irc.IRC` (`IRCUser`) or `twisted.conch.telnet.Telnet` (`CVE-2026-115776` / `GHSA-53xc-49m8-p798` and `CVE-2026-115778` / `GHSA-cwm5-jh59-mwxr`)
  • HTTP servers and reverse-proxied web applications built on `twisted.web.http.HTTPChannel` (`CVE-2026-115877` / `GHSA-q7qq-6mhp-9rc4`), `twisted.mail.imap4` servers (`CVE-2026-115774` / `GHSA-2qrf-pqgx-qh9v`), and `twisted.names.dns.DNSDatagramProtocol` resolvers (`CVE-2026-83813` / `GHSA-jj36-9pvh-vj5r`)

TL;DR

On 22 and 23 October 2026, the Twisted project and NVD published six security advisories affecting all stable releases of Twisted (<= 26.4.0) and fixed in Twisted 26.10.0rc1:

  1. CVE-2026-115775 (GHSA-3476-c5c2-fr73, CVSS v4.0 8.7 High): In twisted.words.xish.domish, both ExpatElementStream and SuxElementStream appended every child element to self.currElem.children and retained the entire tree in memory until the top-level stanza’s closing tag arrived. Because XMPP parses XML streams before authentication, an unauthenticated peer that opens <stream><message> and streams <c/> tags without ever closing </message> exhausts server memory over a single connection.
  2. CVE-2026-115776 (GHSA-53xc-49m8-p798, CVSS 7.5 High) and CVE-2026-115778 (GHSA-cwm5-jh59-mwxr, CVSS 7.5 High): Unlike twisted.protocols.basic.LineReceiver (which enforces MAX_LENGTH = 16384), twisted.words.protocols.irc.IRC.dataReceived() accumulated newline-free bytes into self.buffer with no length cap, and twisted.conch.telnet.Telnet.dataReceived() appended every byte after IAC SB (\xff\xfa) into self.commands until IAC SE (\xff\xf0) arrived. Both run before authentication.
  3. CVE-2026-115877 (GHSA-q7qq-6mhp-9rc4, CVSS 5.4 Medium): twisted.web.http.HTTPChannel.headerReceived() checked header values for b"\x00" (NUL) after stripping obs-fold continuation whitespace, but accepted bare carriage returns (\r) and bare line feeds (\n). Because HTTPChannel only split header lines on \r\n, an upstream proxy that split headers on bare \n or \r disagreed with Twisted on where a header ended, enabling Content-Length / Transfer-Encoding HTTP request smuggling.
  4. CVE-2026-115774 (GHSA-2qrf-pqgx-qh9v, CVSS 4.9 Medium) and CVE-2026-83813 (GHSA-jj36-9pvh-vj5r, CVSS 3.1 Low): In twisted.mail.imap4.parseNestedParens(), a negative inline literal length such as {-100} in an IMAP SEARCH command drove the cursor i negative and locked the single-threaded Twisted reactor at 100% CPU forever. Separately, twisted.names.dns.DNSDatagramProtocol matched UDP DNS replies solely by the 16-bit transaction ID without checking the sender’s (host, port) tuple.

What to do now: Upgrade Twisted to >= 26.10.0rc1 (or apply the four small commit diffs below if your deployment pins a stable release wheel until 26.10.0 final is tagged).

Affected packages and projects

EcosystemPackageCVE / GHSASeverityAffected versionsFixed versionModule
PyPITwistedCVE-2026-115775 (GHSA-3476-c5c2-fr73)CVSS v4.0 8.7 High<= 26.4.026.10.0rc1src/twisted/words/xish/domish.py (ExpatElementStream, SuxElementStream)
PyPITwistedCVE-2026-115776 (GHSA-53xc-49m8-p798)CVSS v3.1 7.5 High<= 26.4.026.10.0rc1src/twisted/words/protocols/irc.py (IRC.dataReceived)
PyPITwistedCVE-2026-115778 (GHSA-cwm5-jh59-mwxr)CVSS v3.1 7.5 High<= 26.4.026.10.0rc1src/twisted/conch/telnet.py (Telnet.dataReceived)
PyPITwistedCVE-2026-115877 (GHSA-q7qq-6mhp-9rc4)CVSS v3.1 5.4 Medium<= 26.4.026.10.0rc1src/twisted/web/http.py (HTTPChannel.headerReceived)
PyPITwistedCVE-2026-115774 (GHSA-2qrf-pqgx-qh9v)CVSS v3.1 4.9 Medium<= 26.4.026.10.0rc1src/twisted/mail/imap4.py (parseNestedParens)
PyPITwistedCVE-2026-83813 (GHSA-jj36-9pvh-vj5r)CVSS v3.1 3.1 Low<= 26.4.026.10.0rc1src/twisted/names/dns.py (DNSDatagramProtocol.datagramReceived)

Technical analysis

1. CVE-2026-115775: unbounded in-memory DOM tree in twisted.words.xish.domish

Twisted’s XML stream parsers (ExpatElementStream and SuxElementStream in src/twisted/words/xish/domish.py) are designed for streaming protocols like XMPP (Jabber), where a long-lived <stream:stream> document contains a sequence of top-level stanzas (<message>, <presence>, <iq>).

In both parsers, ElementEvent only fires when a first-level child of the document root (self.currElem.parent is None) closes. Until that top-level closing tag arrives, every nested start tag allocates a new Element object and appends it to self.currElem.children:

# src/twisted/words/xish/domish.py (vulnerable ExpatElementStream._onStartElement)
if self.documentStarted == 1:
    if self.currElem != None:
        self.currElem.children.append(e)
        e.parent = self.currElem
    self.currElem = e

Because each 4-byte <a/> tag on the wire instantiates a full Python Element object (with attributes, children, localPrefixes, and defaultNsStack dictionaries), a single unauthenticated client opening <stream:stream><message> and streaming <a/> tags (or a single <x attr="AAAA..."/> attribute) amplifies network traffic into gigabytes of heap memory.

Commit 8873814bec bounds both the raw wire bytes per stanza (maxStanzaSize = 10 * 1024 * 1024, counted inside parse() before Expat or Sux buffers attribute values) and the element count per stanza (maxElementCount = 100000), resetting both counters when the top-level stanza is emitted:

--- a/src/twisted/words/xish/domish.py
+++ b/src/twisted/words/xish/domish.py
@@ -780,6 +780,10 @@ class ExpatElementStream:
+    maxStanzaSize = 10 * 1024 * 1024
+    maxElementCount = 100000
+
     def __init__(self):
         ...
+        self.elementCount = 0
+        self.stanzaSize = 0
 
     def parse(self, buffer):
+        self.stanzaSize += len(buffer)
         try:
             self.parser.Parse(buffer)
         except self.error as e:
             raise ParserError(str(e))
+        if self.stanzaSize > self.maxStanzaSize:
+            raise ParserError("Maximum stanza size exceeded")

2. CVE-2026-115776 and CVE-2026-115778: missing pre-auth buffer caps in IRC and Conch Telnet

Both twisted.words.protocols.irc.IRC and twisted.conch.telnet.Telnet subclass protocol.Protocol directly rather than LineReceiver:

  • IRC (CVE-2026-115776, commit 01f8cff775): IRC.dataReceived() concatenated self.buffer = self.buffer + data and split on \n. If a peer streamed bytes without \n, self.buffer grew without limit. The fix adds MAX_LENGTH = 16384 and calls self.lineLengthExceeded(line) (which closes the transport) whenever len(self.buffer) > self.MAX_LENGTH or any split line exceeds self.MAX_LENGTH.
  • Conch Telnet (CVE-2026-115778, commit 073ba878c6): During Telnet option subnegotiation (IAC SB <option> ... IAC SE), Telnet.dataReceived() transitioned into state = "subnegotiation" and appended every payload byte as a 1-byte element into the self.commands list. The fix introduces _MAX_SUBNEGOTIATION_LENGTH = 4096 and drops the connection via _subnegotiationLengthExceeded() as soon as an unterminated IAC SB reaches 4,096 bytes.

3. CVE-2026-115877: twisted.web HTTP request smuggling via bare CR or LF in header values

In src/twisted/web/http.py, HTTPChannel splits incoming header lines on \r\n (LineReceiver.delimiter = b"\r\n"). When a header line (or multi-line obs-fold header) completes, HTTPChannel.headerReceived(self, line) validates the header name and value.

Before commit 022b6f7feb, headerReceived() rejected NUL bytes (\x00) in the header value data, but allowed bare \r or bare \n bytes:

--- a/src/twisted/web/http.py
+++ b/src/twisted/web/http.py
@@ -2499,7 +2499,9 @@ class HTTPChannel(basic.LineReceiver, policies.TimeoutMixin):
         data = data.strip(b" \t")
-        if b"\x00" in data:
+        # A header value must not contain NUL, CR, or LF (RFC 9110 section 5.5).
+        # A bare CR or LF here would let a lenient proxy smuggle a header.
+        if len(data.translate(None, b"\x00\r\n")) != len(data):
             self._respondToBadRequestAndDisconnect()
             return False

If a reverse proxy in front of twisted.web (or a downstream backend behind a twisted.web proxy) treats a bare \n (LF) or bare \r (CR) as a line terminator, a header such as X-Foo: bar\nContent-Length: 0\r\n is parsed as two headers by the peer (Content-Length: 0) but as a single X-Foo header value by HTTPChannel, desynchronizing message boundaries across the keep-alive connection.

4. CVE-2026-115774: IMAP4 parseNestedParens negative literal size infinite loop

In src/twisted/mail/imap4.py, parseNestedParens() parses parenthesized IMAP argument lists (including SEARCH criteria) and supports inline string literals of the form {N}\r\n<N bytes>:

# src/twisted/mail/imap4.py (vulnerable)
literalSize = int(s[i + 1 : end])          # accepts "-100"
contentStack[-1].append((s[end + 3 : end + 3 + literalSize],))
i = end + 3 + literalSize                  # i becomes negative!

When an authenticated IMAP client sent a001 SEARCH CHARSET UTF-8 {-100}\r\n..., int(b"-100") evaluated to -100, moving the cursor i backward or negative. Because Python string slicing s[i : i + 1] on a negative index wraps around the end of the string without raising IndexError and i < len(s) remains True, the synchronous while loop spun forever and froze the entire Twisted reactor process. Commit 3d125548bc rejects any literal size < 0 (or containing a sign character) with IllegalSyntaxError.

Detection and remediation

# 1. Check installed Twisted version in active Python environments
pip show Twisted 2>/dev/null || true

# 2. Search lockfiles and manifests for Twisted <= 26.4.0
rg -n -i '"?twisted"?\s*([~=!<>]=|:)' requirements*.txt pyproject.toml uv.lock poetry.lock Pipfile.lock 2>/dev/null

# 3. Check whether your codebase imports any of the six affected Twisted protocol modules
rg -n 'twisted\.(words\.(xish\.domish|protocols\.irc)|conch\.telnet|web\.http|mail\.imap4|names\.dns)' --glob '*.py' . 2>/dev/null

To remediate:

  1. Upgrade to Twisted >= 26.10.0rc1 (pip install -U "Twisted>=26.10.0rc1", or upgrade to 26.10.0 stable once tagged).
  2. If you run twisted.web behind an HTTP reverse proxy and cannot upgrade immediately: configure the edge proxy (nginx, Envoy, HAProxy, or ALB) to strictly reject HTTP requests containing bare \r or bare \n bytes inside header lines (CVE-2026-115877).

Timeline

  • 2026-10-22 (17:26 UTC): The Twisted maintainers publish Twisted 26.10.0rc1 to PyPI and GitHub (twisted-26.10.0rc1).
  • 2026-10-22 to 2026-10-23 (20:16 to 20:46 UTC): GitHub and NVD publish CVE-2026-115774 (GHSA-2qrf-pqgx-qh9v), CVE-2026-115775 (GHSA-3476-c5c2-fr73), CVE-2026-115776 (GHSA-53xc-49m8-p798), CVE-2026-115778 (GHSA-cwm5-jh59-mwxr), CVE-2026-115877 (GHSA-q7qq-6mhp-9rc4), and CVE-2026-83813 (GHSA-jj36-9pvh-vj5r).

From research to remediation

Check whether this pattern exists in your codebase

Turn this research into a remediation workflow. Scan dependencies and package manifests for similar supply-chain risk, then prioritize fixes with reachability context.

References