Jodit has incomplete javascript: scheme normalization in sanitizeHTMLElement href check that allows link XSS
Collected exploitation and prediction evidence
Risk source coverageCISA KEV is observed exploitation evidence. FIRST EPSS is a dated prediction of exploitation in the next 30 days. Missing scores are unknown. This collector requests predictions only for explicit CVE aliases in the public NuGet archive; Microsoft release views retain their existing prediction scope.
No collected risk observation for these identifiers. This does not establish absence of exploitation or a zero probability.
cve-program · CVE-2026-62324
PublishedJodit Editor is a WYSIWYG editor with a built-in file browser & 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.
- Source published
- 31 Jul 2026 19:08:03 UTC
- Source modified
- 3 Aug 2026 17:26:47 UTC
- Observed here
- 23 Oct 2026 15:48:27 UTC
- Source terms
- CVE Program Terms of Use
Original source record · Cite this exact observation · Complete original publisher JSON
This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: cve-program · CVE Program Terms of Use. Source text is retained; field grouping and change labels are provided by ChangeIntel.
CVSS 3.1 · 5.4 CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N
nvd · CVE-2026-62324
PublishedJodit Editor is a WYSIWYG editor with a built-in file browser & 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.
NVD analysis and CPE criteria are source context, not proof that a package or your environment is affected. Complete affected data, logical configurations and SSVC are retained in the original JSON.
Source analysis status: Deferred
Weakness classifications: CWE-79 · CWE-83
Original publisher date fields
published: 2026-07-31T20:16:53.197
lastModified: 2026-10-23T23:10:00.157
- Source published
- 31 Jul 2026 20:16:53 UTC
- Source modified
- 23 Oct 2026 23:10:00 UTC
- Observed here
- 25 Oct 2026 14:19:28 UTC
- Source terms
- NIST NVD usage terms; underlying source attribution retained
Original source record · Cite this exact observation · Complete original publisher JSON
This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: nvd · NIST NVD usage terms; underlying source attribution retained. Source text is retained; field grouping and change labels are provided by ChangeIntel.
CVSS 3.1 · [email protected] · Secondary · 5.4 CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N
osv · GHSA-j839-gqq4-gf9j
Published### Summary jodit'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: 1. Case variants: `JAVASCRIPT:`, `Javascript:`, `jaVaScRiPt:` (the check is case-sensitive). 2. 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). 3. 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). The 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. ### Details The check is in `sanitizeHTMLElement` at `src/core/helpers/html/safe-html.ts:213`: ```js if (safeJavaScriptLink && href && href.trim().indexOf('javascript') === 0) { attr(elm, 'href', location.protocol + '//' + href); effected = true; } ``` `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). The same file already contains the correct routine, `isDangerousUrl()` (line 176), used for every other URL attribute (`src`, `data`, `action`, `formaction`, `poster`, `background`, `xlink:href`): ```js function isDangerousUrl(value, tagName) { const normalized = value.replace(/[\u0000-\u0020]+/g, '').toLowerCase(); if (/^(?:javascript|vbscript|livescript|mocha):/.test(normalized)) { return true; } // ... } ``` `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` -> `safeHTML` -> `sanitizeHTMLElement`) and the asynchronous on-change path (`sanitizeAttributes` -> `sanitizeHTMLElement`) use that same weak check. Positive 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. ### Proof of concept Default configuration. Assign a payload to the editor and read back the stored value: ```js const editor = Jodit.make('#editor'); editor.value = '<a href="JAVASCRIPT:alert(document.domain)">click me</a>'; // editor.value getter returns the href unchanged: // <p><a href="JAVASCRIPT:alert(document.domain)">click me</a></p> document.getElementById('view').innerHTML = editor.value; // Clicking "click me" runs alert(document.domain) in the consumer's origin. ``` The 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: `<img src=x onerror=...>` stripped; lowercase `javascript:` neutralized to `location.protocol + '//' + href` (`about://...` on the about:blank test page used here, `https://...` on an https page). ### Impact Stored 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. ### Suggested fix Route `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: ```diff - if (safeJavaScriptLink && href && href.trim().indexOf('javascript') === 0) { + if (safeJavaScriptLink && href && isDangerousUrl(href, elm.nodeName.toLowerCase())) { attr(elm, 'href', location.protocol + '//' + href); effected = true; } ```
- Source published
- 31 Jul 2026 19:08:20 UTC
- Source modified
- 31 Jul 2026 19:26:50 UTC
- Observed here
- 24 Oct 2026 13:17:17 UTC
- Source terms
- GitHub Advisory Database · CC-BY-4.0
Original source record · Cite this exact observation · Complete original publisher JSON
This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: osv · GitHub Advisory Database · CC-BY-4.0. Source text is retained; field grouping and change labels are provided by ChangeIntel.
CVSS_V3 · CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N
Publisher rating · MODERATE