Hoch

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

  • Veröffentlicht: 31.07.2026
  • Aktualisiert: 11.10.2026 10:22

Summary

jodit's built-in clean-html sanitizer can be bypassed by a MathML/<style> carrier that hides a dangerous element from the sanitizer's element walk, so a no-interaction event handler survives into the editor value. When an application supplies attacker-influenced HTML to the editor's value-set or insertion paths, the sanitized output still contains a live <img ... onload=...> (or another non-onerror handler such as onfocus). A consumer that renders that output (element.innerHTML = editor.value) executes the handler with no user interaction. This is a stored cross-site scripting vulnerability, confirmed live on the shipped es2021/jodit.min.js for 4.12.25 and the latest 4.12.27 (in Chromium via a client-side innerHTML consumer, and in Firefox via server-rendered / document-context output; see the cross-browser note under Proof of concept).

Details

The bypass exploits the order in which clean-html parses, walks, and re-serializes the value.

  1. On the value-set path, the clean-html plugin handles :beforeSetNativeEditorValue (src/plugins/clean-html/clean-html.ts:116), parsing the value into an inert document: sandBox.innerHTML = data.value.

  2. In that parse, the source nesting math > mtext > table > mglyph > style triggers MathML text-integration-point and foster-parenting rules: the <img> is parsed as text inside <style> (rawtext), not as an element. The <table> is foster-parented out, and the <mglyph> MathML text-integration point governs the namespace, so the <img> never becomes an element node in this parse.

  3. That value-set sanitizer is safeHTML (src/core/helpers/html/safe-html.ts:24); it walks the tree but acts on elements only (the Dom.isElement gate at :39) and runs against the parse-1 sandBox, in which the <img> is rawtext, not an element. So removeAllEventAttributes (the full on* strip at safe-html.ts:76) has no element to clean and the handler passes through. The onBeforeSetNativeEditorValue handler runs safeHTML on that parse-1 tree both before and after it captures the value, so neither pass ever sees the <img> as an element.

  4. The captured value (data.value = sandBox.innerHTML) is then assigned to the editable, a second parse, which hoists the <img> out of <style> and into the HTML namespace as a live element with its handler intact. The serialize-reparse moves the element across the tree and across namespaces:

BEFORE - parse 1 (sandBox): the <img> is <style> text
  <math>          [MathML]
    <mtext>       [MathML]
      <mglyph>    [HTML]      integration point: content parses as HTML
        <style>   [HTML]      text "<img ... onload=...>"   <-- <img> is RAWTEXT, not an element
      <table>     [HTML]

AFTER - editor.value (re-parsed): the <img> is hoisted OUT of <style>, live
  <p>             [HTML]
    <math>        [MathML]
      <mtext>     [MathML]
        <mglyph>  [MathML]
          <style> [MathML]    (now empty)
        <img>     [HTML]      <-- hoisted out, HTML namespace, LIVE -> its handler fires
        <table>   [HTML]
  1. editor.value now carries that live element. The value-set pass (Steps 1-4) only walked the parse-1 sandBox and never sees it; the other sanitizer, the on-change visitor (visitNodeWalker via a LazyWalker, clean-html.ts:56/:70), does reach the hoisted element, but its sanitizeAttributes filter calls sanitizeHTMLElement (safe-html.ts:139), which strips onerror only - it never reads the removeEventAttributes flag sanitizeAttributes passes it (sanitize-attributes.ts:30), so it never runs the full on* strip. So onload, onfocus, and every other non-onerror handler is never removed and persists in editor.value permanently. (onerror is the one handler the cleaner removes, but only after a ~300ms window in which it too fires.)

The two code points (jodit 4.12.27):

js
// 1. clean-html.ts onBeforeSetNativeEditorValue - the SYNCHRONOUS value-set pass runs on the parse-1 sandBox:
sandBox.innerHTML = data.value;                                       // :128  parse 1: the carrier hides the element as <style> rawtext
this.j.e.fire('safeHTML', sandBox);                                   // :129  safeHTML element-walk misses the rawtext element
data.value = sandBox.innerHTML;                                       // :130  value captured; re-parsing it into the editable hoists the element live
safeHTML(sandBox, { safeJavaScriptLink: true, removeOnError: true }); // :131  re-runs on the SAME parse-1 sandBox, never on the captured value

// 2. the ASYNC on-change filter (LazyWalker) reaches the hoisted element, but only strips onerror:
sanitizeHTMLElement(nodeElm, { /* ... */ removeEventAttributes: opts.removeEventAttributes });  // sanitize-attributes.ts:30 - passes the full-strip flag
export function sanitizeHTMLElement(elm, { safeJavaScriptLink, removeOnError } = { /* ... */ }) {  // safe-html.ts:139 - never destructures removeEventAttributes
  if (removeOnError && elm.hasAttribute('onerror')) attr(elm, 'onerror', null);  // onerror ONLY; onload / onfocus / ... are left live
}

All four layers are required: removing any of math + the integration point, table, the mglyph slot, or the rawtext element makes jodit strip the handler (substitutes per slot are under Carrier variants).

The four-layer carrier is required only on 4.11.2 and later. jodit 4.11.2 added cleanHTML.removeEventAttributes (the value-set full on* strip); before it (all 3.x and 4.0 through 4.10.x) the sanitizer only ever removed onerror, so on those versions a plain non-onerror handler such as <img ... onload=...> survives editor.value directly with no carrier (live-confirmed on 3.24.9, 4.0.1, 4.2.27). On 4.11.2 and later, the value-set walk strips the bare handler, so the carrier is needed to hide it as <style> rawtext past that walk; and because the on-change cleaner removes only onerror (Step 5), a non-onerror hoisted handler survives across the whole range.

The bypass is not specific to the value setter. The same carrier survives clean-html through editor.value = X, editor.setEditorValue(X), and editor.s.insertHTML(X) (the API jodit's own documentation uses for plugins and custom buttons).

This is distinct from jodit's known XSS advisories: CVE-2023-42399 (GHSA-95xr-cq6h-vwr3) is an iframe[src] URL-scheme issue fixed in a 4.0.0 beta, and CVE-2022-23461 (GHSA-42hx-vrxx-5r6v) is a paste-from-Word onerror desanitization in <= 3.24.2. Neither involves this parse-1 rawtext-hoist mechanism, and a search of the issue tracker for mglyph / mathml / mutation / "value xss" finds no prior report.

Proof of concept

Default configuration. Assign the payload, read it back, render it the way a consumer would:

js
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
// -> the 1x1 gif loads, onload fires alert(document.domain), no interaction

Cross-browser: editor.value carries the hoisted live <img> in both Blink and Gecko, but the consumer's parse mode decides execution. Chromium fires it under both client-side element.innerHTML and document parsing; Firefox fires it only under document parsing (server-side rendering, document.write, <iframe srcdoc>), because under innerHTML Gecko leaves the <img> in the MathML namespace, inert. No iframe is required: a plain innerHTML consumer suffices in Chromium, any server-rendered consumer in Firefox.

Positive control, same run: a plain <img src=x onerror=alert(1)>, a plain <img onfocus=alert(1) autofocus tabindex=1>, and a plain <svg onload=alert(1)> are all stripped by the synchronous value-set pass to harmless output, proving clean-html is active; the same handlers pass through only when carrier-hidden as <style> rawtext, so the contrast isolates the rawtext-hiding step. (A plain <script> is a separate case - the value-set pass does not remove tags; only the async on-change cleaner removes it ~300ms later - so it is not part of this synchronous control.)

Carrier variants: three of the four layers accept substitutes: the MathML text-integration point (mtext / mi / ms / mn / mo), the integration-point child (mglyph / malignmark), and the rawtext element (style / xmp / noembed / script / plaintext; title / textarea / noscript do not work). math and table have no working substitute. The hidden element is not limited to <img>, and any non-onerror handler persists permanently. Element-name blocklisting will not close this.

Impact

Stored XSS with no user interaction, in the default configuration, on the input paths that jodit's own clean-html is responsible for sanitizing. jodit's own test suite asserts this is a sanitization boundary: src/plugins/clean-html/clean-html.test.js asserts that editor.value = '<p>test <img src="" onerror="alert(111)" alt=""></p>' sanitizes to <p>test <img src="" alt=""></p> (the onerror removed) under the default config. The carrier in this report passes that same default-config sanitizer yet keeps the handler live, defeating the asserted guarantee. A prior fix in 4.12.21 addressed a stored XSS premised on an application re-rendering editor.value as trusted HTML, so this threat model is maintainer-acknowledged. Precondition: an attacker can place HTML into the editor (a content-submission role) and the editor output is later rendered. Any integration binding value to application state in jodit-react, loading a previously stored document, or inserting content via a plugin/button will execute attacker script in the victim's page.

Suggested fix

Remove the gadget element at the source rather than re-sanitizing the output. A re-sanitize loop does not close this: nesting the carrier inside itself surfaces one level per parse, so one extra pass is bypassed at depth 2, and any fixed cap N is out-nested at depth N+2 (depth generalizes trivially). The robust fix is to drop any HTML-namespace element smuggled inside <math>/<svg> outside a spec integration point (<foreignObject>, <annotation-xml>, <desc>, <title>) during the element walk, the same approach DOMPurify's _checkValidNamespace uses. This is one pass and depth-independent. It must run on both entry points: the synchronous value-set safeHTML pass and the on-change walker (which does not call safeHTML). A regression test asserting that no on* survives a round-trip through editor.value, including the nested-carrier case, locks it in.

Betroffene Software

Klicken Sie auf eine Technologie, um alle zugehörigen Schwachstellen anzuzeigen.

PlattformPaketAnfällige VersionenBehoben in
npm-Paket jodit < 4.12.28 4.12.28

Verweise

CVSS-Wert 7.2/10
CVSS-Vektor
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N
Schwäche (CWE)
CWE-79, CWE-83
Plattform
npm
Paket
jodit
Veröffentlicht
31.07.2026
Aktualisiert
11.10.2026 10:22

Quellen und Lizenzen

OSV / GitHub Advisory Database - GHSA-rxcw-mc6f-6hr3 - CC BY 4.0

Schwachstellenhistorie: jodit

Alle veröffentlichten Schwachstellen dieses Pakets (npm-Paket), neueste zuerst. Zeigt, ob es ein einmaliger Ausrutscher ist oder ein Paket, das man im Auge behalten muss.

Einträge insgesamt
7
Erste → letzte
09.2022 07.2026
Ohne Fix
2
In Angriffen ausgenutzt
0

Verteilung nach Schweregrad

  • Hoch 1
  • Mittel 6

Veröffentlichungen pro Jahr

  1. 2022: 1
  2. 2023: 1
  3. 2024: 0
  4. 2025: 0
  5. 2026: 5
  1. Jodit Editor: Mutation XSS in jodit clean-html via a MathML/style rawtext carrier dieser Eintrag
    Hoch · 7.2 CVE-2026-58263 < 4.12.28 4.12.28
  2. Mittel · 6.1 CVE-2023-42399 = 4.0.0-beta.86 keine Korrektur
  3. Mittel · 6.1 CVE-2022-23461 <= 3.24.2 keine Korrektur

Erkennen Sie Schwachstellen in Ihrer Anwendung

Mit der DockRay-Integration und aktivierter Softwareinventarisierung prüft DockRay gemeldete Komponentenversionen auf bekannte Schwachstellen und benachrichtigt Sie über bestätigte Treffer. Die Abdeckung hängt von der unterstützten Technologie und den verfügbaren Meldungen ab.

Anwendung verbinden
DockRay Newsletter

Anwendungssicherheit in Ihrem Postfach

Wichtige Schwachstellen, konkrete Empfehlungen und praktische Tipps für Ihre Anwendungen. Wählen Sie Ihre Technologien und verfolgen Sie, was für Ihr Projekt relevant ist.

  • Eine tägliche Zusammenfassung neuer Schwachstellen, auch kritischer.
  • Benachrichtigungen über wichtige Updates.
  • Praktische Tipps zur Sicherheit und Überwachung von Anwendungen.

Leer lassen, um Meldungen zu allen Technologien zu erhalten.

Kostenlos anmelden. In jeder E-Mail mit einem Klick abmelden.