{"id":"CVE-2026-62324","aliases":["EUVD-2026-51621","GHSA-j839-gqq4-gf9j"],"status":"published","title":"Jodit has incomplete javascript: scheme normalization in sanitizeHTMLElement href check that allows link XSS","description":"Jodit Editor is a WYSIWYG editor with a built-in file browser \u0026 image editor. Prior to 4.12.31, Jodit's sanitizeHTMLElement method fails to use isDangerousUrl to normalize javascript: href values before checking the scheme, allowing case variants, control-byte prefixes, and embedded tabs or newlines to bypass filtering and execute attacker-controlled script when a victim clicks a stored link rendered by an application. This issue is fixed in version 4.12.31.","severity":{"version":"3.1","score":5.4,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-09","score":0.00313,"percentile":0.24425,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-79","CWE-83"],"published":"2026-07-31T19:08:03Z","modified":"2026-10-28T02:30:36Z","fixes":[{"ecosystem":"npm","package":"jodit","fixed_in":["4.12.31"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"npm","package":"jodit","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"4.12.30"},{"fixed":"4.12.31"}]}]},{"source":"osv","ecosystem":"npm","package":"jodit","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"4.12.31"}]}]}],"products":[{"vendor":"xdan","product":"jodit"}],"references":[{"url":"https://github.com/xdan/jodit/security/advisories/GHSA-j839-gqq4-gf9j","tags":["x_refsource_CONFIRM","exploit","advisory","web"]},{"url":"https://github.com/xdan/jodit/commit/5fba6ef2381d151d7cb8e3c5ad0b9996af0f97b0","tags":["x_refsource_MISC","fix","web"]},{"url":"https://github.com/xdan/jodit/releases/tag/4.12.31","tags":["x_refsource_MISC","web"]},{"url":"https://github.com/advisories/GHSA-j839-gqq4-gf9j"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/62xxx/CVE-2026-62324.json","tags":["advisory"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-62324","tags":["advisory"]},{"url":"https://github.com/xdan/jodit","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-62324","title":"Jodit has incomplete javascript: scheme normalization in sanitizeHTMLElement href check that allows link XSS","description":"Jodit Editor is a WYSIWYG editor with a built-in file browser \u0026 image editor. Prior to 4.12.31, Jodit's sanitizeHTMLElement method fails to use isDangerousUrl to normalize javascript: href values before checking the scheme, allowing case variants, control-byte prefixes, and embedded tabs or newlines to bypass filtering and execute attacker-controlled script when a victim clicks a stored link rendered by an application. This issue is fixed in version 4.12.31.","status":"published","published":"2026-07-31T19:08:03.043Z","modified":"2026-08-03T17:26:47.204Z","cvss":[{"version":"3.1","score":5.4,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N","level":"medium","assessor":"cna"}],"cwes":["CWE-79","CWE-83"],"updated_at":"2026-11-02T14:20:14.665567Z"}],"euvd":[{"id":"EUVD-2026-51621","description":"Jodit Editor is a WYSIWYG editor with a built-in file browser \u0026 image editor. Prior to 4.12.31, Jodit's sanitizeHTMLElement method fails to use isDangerousUrl to normalize javascript: href values before checking the scheme, allowing case variants, control-byte prefixes, and embedded tabs or newlines to bypass filtering and execute attacker-controlled script when a victim clicks a stored link rendered by an application. This issue is fixed in version 4.12.31.","status":"published","published":"2026-07-31T19:08:03Z","modified":"2026-08-03T17:26:47Z","cvss":[{"version":"3.1","score":5.4,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N","level":"medium"}],"updated_at":"2026-11-02T18:02:24.01774Z"}],"ghsa":[{"id":"GHSA-j839-gqq4-gf9j","title":"Jodit has incomplete javascript: scheme normalization in sanitizeHTMLElement href check that allows link XSS","description":"### Summary\n\njodit's `sanitizeHTMLElement` neutralizes a `javascript:` `href` using a bare `href.trim().indexOf('javascript') === 0` check. This omits the normalization jodit applies to every other URL attribute: `isDangerousUrl` strips control bytes with `value.replace(/[\\u0000-\\u0020]+/g, '')` and lowercases the value before testing the scheme. Because the `href` check does neither, it is bypassed by three obfuscation classes, all confirmed firing on click against the shipped 4.12.30 build:\n\n1. Case variants: `JAVASCRIPT:`, `Javascript:`, `jaVaScRiPt:` (the check is case-sensitive).\n2. A leading C0 control byte, e.g. a `\\x01` prefix before lowercase `javascript:` (`trim()` does not remove bytes in the `\\x00`-`\\x08` / `\\x0e`-`\\x1f` range, but the browser strips a leading control byte before resolving the scheme).\n3. An embedded tab or newline inside the scheme, e.g. `java\\tscript:` or `java\\nscript:` (the browser strips tab/newline from a URL, but `indexOf('javascript')` sees the broken word and does not match).\n\nThe dangerous href survives `editor.value =` assignment and the on-change LazyWalker, persisting in the stored editor value. A victim who clicks the link in any consumer that renders the stored value (readonly editor, server-rendered page, `innerHTML` consumer) runs attacker-controlled JS in that page's origin.\n\n### Details\n\nThe check is in `sanitizeHTMLElement` at `src/core/helpers/html/safe-html.ts:213`:\n\n```js\nif (safeJavaScriptLink \u0026\u0026 href \u0026\u0026 href.trim().indexOf('javascript') === 0) {\n    attr(elm, 'href', location.protocol + '//' + href);\n    effected = true;\n}\n```\n\n`href.trim()` removes leading/trailing ASCII whitespace only, and `indexOf('javascript')` is case-sensitive and literal. So the check fails to fire whenever the scheme is upper/mixed-case, prefixed by a non-whitespace control byte, or split by an embedded tab/newline - all of which a browser still resolves to `javascript:` on click (URI schemes are case-insensitive per RFC 3986 section 3.1; leading control bytes, tabs and newlines are stripped from a URL during parsing).\n\nThe same file already contains the correct routine, `isDangerousUrl()` (line 176), used for every other URL attribute (`src`, `data`, `action`, `formaction`, `poster`, `background`, `xlink:href`):\n\n```js\nfunction isDangerousUrl(value, tagName) {\n    const normalized = value.replace(/[\\u0000-\\u0020]+/g, '').toLowerCase();\n    if (/^(?:javascript|vbscript|livescript|mocha):/.test(normalized)) {\n        return true;\n    }\n    // ...\n}\n```\n\n`isDangerousUrl` strips every control byte and ASCII space (`/[\\u0000-\\u0020]+/g`) and lowercases before testing the scheme, so it resists all three obfuscations. But `href` never goes through it: the attribute list `isDangerousUrl` is applied to (`URL_ATTRIBUTES`) is commented \"besides href\", and `href` is handled only by the weaker `indexOf` check. Both the synchronous value-set path (`onBeforeSetNativeEditorValue` -\u003e `safeHTML` -\u003e `sanitizeHTMLElement`) and the asynchronous on-change path (`sanitizeAttributes` -\u003e `sanitizeHTMLElement`) use that same weak check.\n\nPositive controls (filter is otherwise live): a plain lowercase `javascript:` href IS neutralized: jodit rewrites the value to `location.protocol + '//' + href`, so it reads `about://javascript:...` on an about:blank test page and `https://javascript:...` on an https page. A leading ASCII space or tab IS caught by `trim()`; the bypass is specific to the un-normalized forms above.\n\n### Proof of concept\n\nDefault configuration. Assign a payload to the editor and read back the stored value:\n\n```js\nconst editor = Jodit.make('#editor');\neditor.value = '\u003ca href=\"JAVASCRIPT:alert(document.domain)\"\u003eclick me\u003c/a\u003e';\n// editor.value getter returns the href unchanged:\n//   \u003cp\u003e\u003ca href=\"JAVASCRIPT:alert(document.domain)\"\u003eclick me\u003c/a\u003e\u003c/p\u003e\ndocument.getElementById('view').innerHTML = editor.value;\n// Clicking \"click me\" runs alert(document.domain) in the consumer's origin.\n```\n\nThe same persists for the leading-control-byte form (a `\\x01` prefix before lowercase `javascript:`) and the embedded-tab/newline forms (`java\\tscript:` / `java\\nscript:`). Verified live on the shipped `es2021/jodit.min.js` for jodit 4.12.30. Positive controls in the same run: `\u003cimg src=x onerror=...\u003e` stripped; lowercase `javascript:` neutralized to `location.protocol + '//' + href` (`about://...` on the about:blank test page used here, `https://...` on an https page).\n\n### Impact\n\nStored click-XSS. An attacker with write access to an editor instance (content author, or comment author in a multi-user application) stores a crafted `javascript:` link. Any user who clicks it in a view that renders the stored value (readonly editor, server-rendered page, `innerHTML` consumer) runs attacker-controlled JS in that origin. One user interaction (the click) is required. A consumer that re-sanitizes the editor output before rendering is not affected.\n\n### Suggested fix\n\nRoute `href` through the existing `isDangerousUrl()` rather than the bespoke `indexOf` check. `isDangerousUrl` already strips control bytes and lowercases, so it closes the case, control-byte, and embedded-whitespace bypasses at once:\n\n```diff\n-\tif (safeJavaScriptLink \u0026\u0026 href \u0026\u0026 href.trim().indexOf('javascript') === 0) {\n+\tif (safeJavaScriptLink \u0026\u0026 href \u0026\u0026 isDangerousUrl(href, elm.nodeName.toLowerCase())) {\n \t\tattr(elm, 'href', location.protocol + '//' + href);\n \t\teffected = true;\n \t}\n```","status":"published","published":"2026-07-31T19:08:20Z","modified":"2026-07-31T19:08:21Z","cvss":[{"version":"3.1","score":5.4,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-79","CWE-83"],"updated_at":"2026-11-02T14:40:55.903445Z"}],"nvd":[{"id":"CVE-2026-62324","description":"Jodit Editor is a WYSIWYG editor with a built-in file browser \u0026 image editor. Prior to 4.12.31, Jodit's sanitizeHTMLElement method fails to use isDangerousUrl to normalize javascript: href values before checking the scheme, allowing case variants, control-byte prefixes, and embedded tabs or newlines to bypass filtering and execute attacker-controlled script when a victim clicks a stored link rendered by an application. This issue is fixed in version 4.12.31.","status":"published","published":"2026-07-31T20:16:53.197Z","modified":"2026-10-23T23:10:00.157Z","cvss":[{"version":"3.1","score":5.4,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N","level":"medium","assessor":"secondary"}],"cwes":["CWE-79","CWE-83"],"updated_at":"2026-11-02T14:12:34.290716Z"}],"osv":[{"id":"CVE-2026-62324","title":"Jodit has incomplete javascript: scheme normalization in sanitizeHTMLElement href check that allows link XSS","description":"Jodit Editor is a WYSIWYG editor with a built-in file browser \u0026 image editor. Prior to 4.12.31, Jodit's sanitizeHTMLElement method fails to use isDangerousUrl to normalize javascript: href values before checking the scheme, allowing case variants, control-byte prefixes, and embedded tabs or newlines to bypass filtering and execute attacker-controlled script when a victim clicks a stored link rendered by an application. This issue is fixed in version 4.12.31.","status":"published","published":"2026-07-31T19:08:03.043Z","modified":"2026-10-28T02:30:36.978960753Z","cvss":[{"version":"3.1","score":5.4,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-79","CWE-83"],"updated_at":"2026-11-02T15:45:13.306117Z"},{"id":"GHSA-j839-gqq4-gf9j","title":"Jodit has incomplete javascript: scheme normalization in sanitizeHTMLElement href check that allows link XSS","description":"### Summary\n\njodit's `sanitizeHTMLElement` neutralizes a `javascript:` `href` using a bare `href.trim().indexOf('javascript') === 0` check. This omits the normalization jodit applies to every other URL attribute: `isDangerousUrl` strips control bytes with `value.replace(/[\\u0000-\\u0020]+/g, '')` and lowercases the value before testing the scheme. Because the `href` check does neither, it is bypassed by three obfuscation classes, all confirmed firing on click against the shipped 4.12.30 build:\n\n1. Case variants: `JAVASCRIPT:`, `Javascript:`, `jaVaScRiPt:` (the check is case-sensitive).\n2. A leading C0 control byte, e.g. a `\\x01` prefix before lowercase `javascript:` (`trim()` does not remove bytes in the `\\x00`-`\\x08` / `\\x0e`-`\\x1f` range, but the browser strips a leading control byte before resolving the scheme).\n3. An embedded tab or newline inside the scheme, e.g. `java\\tscript:` or `java\\nscript:` (the browser strips tab/newline from a URL, but `indexOf('javascript')` sees the broken word and does not match).\n\nThe dangerous href survives `editor.value =` assignment and the on-change LazyWalker, persisting in the stored editor value. A victim who clicks the link in any consumer that renders the stored value (readonly editor, server-rendered page, `innerHTML` consumer) runs attacker-controlled JS in that page's origin.\n\n### Details\n\nThe check is in `sanitizeHTMLElement` at `src/core/helpers/html/safe-html.ts:213`:\n\n```js\nif (safeJavaScriptLink \u0026\u0026 href \u0026\u0026 href.trim().indexOf('javascript') === 0) {\n    attr(elm, 'href', location.protocol + '//' + href);\n    effected = true;\n}\n```\n\n`href.trim()` removes leading/trailing ASCII whitespace only, and `indexOf('javascript')` is case-sensitive and literal. So the check fails to fire whenever the scheme is upper/mixed-case, prefixed by a non-whitespace control byte, or split by an embedded tab/newline - all of which a browser still resolves to `javascript:` on click (URI schemes are case-insensitive per RFC 3986 section 3.1; leading control bytes, tabs and newlines are stripped from a URL during parsing).\n\nThe same file already contains the correct routine, `isDangerousUrl()` (line 176), used for every other URL attribute (`src`, `data`, `action`, `formaction`, `poster`, `background`, `xlink:href`):\n\n```js\nfunction isDangerousUrl(value, tagName) {\n    const normalized = value.replace(/[\\u0000-\\u0020]+/g, '').toLowerCase();\n    if (/^(?:javascript|vbscript|livescript|mocha):/.test(normalized)) {\n        return true;\n    }\n    // ...\n}\n```\n\n`isDangerousUrl` strips every control byte and ASCII space (`/[\\u0000-\\u0020]+/g`) and lowercases before testing the scheme, so it resists all three obfuscations. But `href` never goes through it: the attribute list `isDangerousUrl` is applied to (`URL_ATTRIBUTES`) is commented \"besides href\", and `href` is handled only by the weaker `indexOf` check. Both the synchronous value-set path (`onBeforeSetNativeEditorValue` -\u003e `safeHTML` -\u003e `sanitizeHTMLElement`) and the asynchronous on-change path (`sanitizeAttributes` -\u003e `sanitizeHTMLElement`) use that same weak check.\n\nPositive controls (filter is otherwise live): a plain lowercase `javascript:` href IS neutralized: jodit rewrites the value to `location.protocol + '//' + href`, so it reads `about://javascript:...` on an about:blank test page and `https://javascript:...` on an https page. A leading ASCII space or tab IS caught by `trim()`; the bypass is specific to the un-normalized forms above.\n\n### Proof of concept\n\nDefault configuration. Assign a payload to the editor and read back the stored value:\n\n```js\nconst editor = Jodit.make('#editor');\neditor.value = '\u003ca href=\"JAVASCRIPT:alert(document.domain)\"\u003eclick me\u003c/a\u003e';\n// editor.value getter returns the href unchanged:\n//   \u003cp\u003e\u003ca href=\"JAVASCRIPT:alert(document.domain)\"\u003eclick me\u003c/a\u003e\u003c/p\u003e\ndocument.getElementById('view').innerHTML = editor.value;\n// Clicking \"click me\" runs alert(document.domain) in the consumer's origin.\n```\n\nThe same persists for the leading-control-byte form (a `\\x01` prefix before lowercase `javascript:`) and the embedded-tab/newline forms (`java\\tscript:` / `java\\nscript:`). Verified live on the shipped `es2021/jodit.min.js` for jodit 4.12.30. Positive controls in the same run: `\u003cimg src=x onerror=...\u003e` stripped; lowercase `javascript:` neutralized to `location.protocol + '//' + href` (`about://...` on the about:blank test page used here, `https://...` on an https page).\n\n### Impact\n\nStored click-XSS. An attacker with write access to an editor instance (content author, or comment author in a multi-user application) stores a crafted `javascript:` link. Any user who clicks it in a view that renders the stored value (readonly editor, server-rendered page, `innerHTML` consumer) runs attacker-controlled JS in that origin. One user interaction (the click) is required. A consumer that re-sanitizes the editor output before rendering is not affected.\n\n### Suggested fix\n\nRoute `href` through the existing `isDangerousUrl()` rather than the bespoke `indexOf` check. `isDangerousUrl` already strips control bytes and lowercases, so it closes the case, control-byte, and embedded-whitespace bypasses at once:\n\n```diff\n-\tif (safeJavaScriptLink \u0026\u0026 href \u0026\u0026 href.trim().indexOf('javascript') === 0) {\n+\tif (safeJavaScriptLink \u0026\u0026 href \u0026\u0026 isDangerousUrl(href, elm.nodeName.toLowerCase())) {\n \t\tattr(elm, 'href', location.protocol + '//' + href);\n \t\teffected = true;\n \t}\n```","status":"published","published":"2026-07-31T19:08:20Z","modified":"2026-07-31T19:26:50.767101Z","cvss":[{"version":"3.1","score":5.4,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-79","CWE-83"],"updated_at":"2026-11-02T15:46:49.977351Z"}]}}
