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:
CVE-2026-115775(GHSA-3476-c5c2-fr73, CVSS v4.0 8.7 High): Intwisted.words.xish.domish, bothExpatElementStreamandSuxElementStreamappended every child element toself.currElem.childrenand 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.CVE-2026-115776(GHSA-53xc-49m8-p798, CVSS 7.5 High) andCVE-2026-115778(GHSA-cwm5-jh59-mwxr, CVSS 7.5 High): Unliketwisted.protocols.basic.LineReceiver(which enforcesMAX_LENGTH = 16384),twisted.words.protocols.irc.IRC.dataReceived()accumulated newline-free bytes intoself.bufferwith no length cap, andtwisted.conch.telnet.Telnet.dataReceived()appended every byte afterIAC SB(\xff\xfa) intoself.commandsuntilIAC SE(\xff\xf0) arrived. Both run before authentication.CVE-2026-115877(GHSA-q7qq-6mhp-9rc4, CVSS 5.4 Medium):twisted.web.http.HTTPChannel.headerReceived()checked header values forb"\x00"(NUL) after strippingobs-foldcontinuation whitespace, but accepted bare carriage returns (\r) and bare line feeds (\n). BecauseHTTPChannelonly split header lines on\r\n, an upstream proxy that split headers on bare\nor\rdisagreed with Twisted on where a header ended, enablingContent-Length/Transfer-EncodingHTTP request smuggling.CVE-2026-115774(GHSA-2qrf-pqgx-qh9v, CVSS 4.9 Medium) andCVE-2026-83813(GHSA-jj36-9pvh-vj5r, CVSS 3.1 Low): Intwisted.mail.imap4.parseNestedParens(), a negative inline literal length such as{-100}in an IMAPSEARCHcommand drove the cursorinegative and locked the single-threaded Twisted reactor at 100% CPU forever. Separately,twisted.names.dns.DNSDatagramProtocolmatched 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
| Ecosystem | Package | CVE / GHSA | Severity | Affected versions | Fixed version | Module |
|---|---|---|---|---|---|---|
| PyPI | Twisted | CVE-2026-115775 (GHSA-3476-c5c2-fr73) | CVSS v4.0 8.7 High | <= 26.4.0 | 26.10.0rc1 | src/twisted/words/xish/domish.py (ExpatElementStream, SuxElementStream) |
| PyPI | Twisted | CVE-2026-115776 (GHSA-53xc-49m8-p798) | CVSS v3.1 7.5 High | <= 26.4.0 | 26.10.0rc1 | src/twisted/words/protocols/irc.py (IRC.dataReceived) |
| PyPI | Twisted | CVE-2026-115778 (GHSA-cwm5-jh59-mwxr) | CVSS v3.1 7.5 High | <= 26.4.0 | 26.10.0rc1 | src/twisted/conch/telnet.py (Telnet.dataReceived) |
| PyPI | Twisted | CVE-2026-115877 (GHSA-q7qq-6mhp-9rc4) | CVSS v3.1 5.4 Medium | <= 26.4.0 | 26.10.0rc1 | src/twisted/web/http.py (HTTPChannel.headerReceived) |
| PyPI | Twisted | CVE-2026-115774 (GHSA-2qrf-pqgx-qh9v) | CVSS v3.1 4.9 Medium | <= 26.4.0 | 26.10.0rc1 | src/twisted/mail/imap4.py (parseNestedParens) |
| PyPI | Twisted | CVE-2026-83813 (GHSA-jj36-9pvh-vj5r) | CVSS v3.1 3.1 Low | <= 26.4.0 | 26.10.0rc1 | src/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, commit01f8cff775):IRC.dataReceived()concatenatedself.buffer = self.buffer + dataand split on\n. If a peer streamed bytes without\n,self.buffergrew without limit. The fix addsMAX_LENGTH = 16384and callsself.lineLengthExceeded(line)(which closes the transport) wheneverlen(self.buffer) > self.MAX_LENGTHor any split line exceedsself.MAX_LENGTH. - Conch Telnet (
CVE-2026-115778, commit073ba878c6): During Telnet option subnegotiation (IAC SB <option> ... IAC SE),Telnet.dataReceived()transitioned intostate = "subnegotiation"and appended every payload byte as a 1-byte element into theself.commandslist. The fix introduces_MAX_SUBNEGOTIATION_LENGTH = 4096and drops the connection via_subnegotiationLengthExceeded()as soon as an unterminatedIAC SBreaches 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:
- Upgrade to
Twisted >= 26.10.0rc1(pip install -U "Twisted>=26.10.0rc1", or upgrade to26.10.0stable once tagged). - If you run
twisted.webbehind an HTTP reverse proxy and cannot upgrade immediately: configure the edge proxy (nginx, Envoy, HAProxy, or ALB) to strictly reject HTTP requests containing bare\ror bare\nbytes inside header lines (CVE-2026-115877).
Timeline
- 2026-10-22 (17:26 UTC): The Twisted maintainers publish
Twisted 26.10.0rc1to 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), andCVE-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
- Twisted advisory GHSA-3476-c5c2-fr73 (CVE-2026-115775: domish XML element stream unbounded tree)
- Twisted advisory GHSA-53xc-49m8-p798 (CVE-2026-115776: IRC server unbounded line buffer)
- Twisted advisory GHSA-cwm5-jh59-mwxr (CVE-2026-115778: Conch Telnet unbounded subnegotiation buffer)
- Twisted advisory GHSA-q7qq-6mhp-9rc4 (CVE-2026-115877: twisted.web bare CR/LF HTTP request smuggling)
- Twisted advisory GHSA-2qrf-pqgx-qh9v (CVE-2026-115774: IMAP4 negative literal size infinite loop)
- twisted/twisted commit 8873814bec (CVE-2026-115775 fix)
- twisted/twisted commit 022b6f7feb (CVE-2026-115877 fix)
- Twisted release twisted-26.10.0rc1