Answer in brief
CVE-2026-58263 records a High severity (CVSS 7.2) xss vulnerability in Jodit Editor: Mutation XSS in jodit clean-html via a MathML/style rawtext carrier. The source record does not mark it as known exploited. 1 affected package is mapped in the feed.
Answer in brief
CVE-2026-58263 records a High severity (CVSS 7.2) xss vulnerability in Jodit Editor: Mutation XSS in jodit clean-html via a MathML/style rawtext carrier. The source record does not mark it as known exploited. 1 affected package is mapped in the feed.
Update jodit to 4.12.28 if you use the affected versions. Test the change in a non-production environment first.
Local check
hol-guard supply-chain scanXSS describes the vulnerability class recorded for this advisory. The current record does not mark CVE-2026-58263 as known exploited; continue to monitor the source for status changes. The feed includes package mappings that can be checked against lockfiles and deployed manifests.
| Package | Affected range | Fixed version |
|---|---|---|
| joditnpm | <4.12.28 | 4.12.28 |
Fixed versions are reported by the source feed; confirm compatibility before updating.
Reported by GitHub Security Advisories (ghsa).
CVE-2026-58263 records a High severity (CVSS 7.2) xss vulnerability in Jodit Editor: Mutation XSS in jodit clean-html via a MathML/style rawtext carrier. The source record does not mark it as known exploited. 1 affected package is mapped in the feed.
The source record does not mark it as known exploited.
Check lockfiles and deployed manifests for jodit.
HOL Guard can help your team review package activity against supported protection paths.
Explore HOL GuardUpdate jodit to 4.12.28 if you use the affected versions. Test the change in a non-production environment first.
Local check
hol-guard supply-chain scanXSS describes the vulnerability class recorded for this advisory. The current record does not mark CVE-2026-58263 as known exploited; continue to monitor the source for status changes. The feed includes package mappings that can be checked against lockfiles and deployed manifests.
| Package | Affected range | Fixed version |
|---|---|---|
| joditnpm | <4.12.28 | 4.12.28 |
Fixed versions are reported by the source feed; confirm compatibility before updating.
Reported by GitHub Security Advisories (ghsa).
CVE-2026-58263 records a High severity (CVSS 7.2) xss vulnerability in Jodit Editor: Mutation XSS in jodit clean-html via a MathML/style rawtext carrier. The source record does not mark it as known exploited. 1 affected package is mapped in the feed.
The source record does not mark it as known exploited.
Check lockfiles and deployed manifests for jodit.
HOL Guard can help your team review package activity against supported protection paths.
Explore HOL Guard### 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] ``` 5. `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.
### 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] ``` 5. `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.