Open-Source Security Intelligence

Know every vulnerability
before it knows you.

DevGuard continuously monitors your dependencies and alerts you when CVEs like this one affect your stack — with real-time threat intelligence built for developers.

Search

GHSA-rxcw-mc6f-6hr3

HighCVSS 7.2 / 10
Published Jul 31, 2026·Last modified Jul 31, 2026
Affected Components(0)

No affected components available

Description

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):

// 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:

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.

Risk Scores
Base Score
7.2

The vulnerability can be exploited over the network without needing physical access. It is easy for an attacker to exploit this vulnerability. An attacker does not need any special privileges or access rights. No user interaction is needed for the attacker to exploit this vulnerability. The vulnerability can affect other systems as well, not just the initial system. There is a low impact on the confidentiality of the information. There is a low impact on the integrity of the data.

Threat Intelligence
6.6

Exploitation attempts have been detected. Elevated vigilance and prompt remediation are advised.

EPSS
0.18%

The exploit probability is very low. The vulnerability is unlikely to be exploited in the next 30 days.

Exploit
Not available

We did not find any exploit available. Neither in GitHub repositories nor in the Exploit-Database.

Browse More

Scan your project

Continuously monitor your dependencies and get alerted when vulnerabilities like this one affect your stack.

Checkout DevGuard