{"id":"GHSA-j839-gqq4-gf9j","summary":"Jodit has incomplete javascript: scheme normalization in sanitizeHTMLElement href check that allows link XSS","details":"### 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 && href && 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 && href && href.trim().indexOf('javascript') === 0) {\n+\tif (safeJavaScriptLink && href && isDangerousUrl(href, elm.nodeName.toLowerCase())) {\n \t\tattr(elm, 'href', location.protocol + '//' + href);\n \t\teffected = true;\n \t}\n```","aliases":["CVE-2026-62324"],"modified":"2026-07-31T19:26:50.767101Z","published":"2026-07-31T19:08:20Z","database_specific":{"nvd_published_at":null,"cwe_ids":["CWE-79","CWE-83"],"severity":"MODERATE","github_reviewed":true,"github_reviewed_at":"2026-07-31T19:08:20Z"},"references":[{"type":"WEB","url":"https://github.com/xdan/jodit/security/advisories/GHSA-j839-gqq4-gf9j"},{"type":"WEB","url":"https://github.com/xdan/jodit/commit/5fba6ef2381d151d7cb8e3c5ad0b9996af0f97b0"},{"type":"PACKAGE","url":"https://github.com/xdan/jodit"},{"type":"WEB","url":"https://github.com/xdan/jodit/releases/tag/4.12.31"}],"affected":[{"package":{"name":"jodit","ecosystem":"npm","purl":"pkg:npm/jodit"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"4.12.31"}]}],"database_specific":{"last_known_affected_version_range":"\u003c= 4.12.30","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-j839-gqq4-gf9j/GHSA-j839-gqq4-gf9j.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N"}]}