GHSA-j839-gqq4-gf9j Jodit has incomplete javascript: scheme normalization in sanitizeHTMLElement href check that allows link XSS
Summaryjodit'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:Case variants: `JAVASCRIPT:`, `Javascript:`, `jaVaScRiPt:` (the check is case-sensitive).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).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.DetailsThe check is in `sanitizeHTMLElement` at `src/core/helpers/html/safe-html.ts:213`:`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`):`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 conceptDefault configuration. Assign a payload to the editor and read back the stored value: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).ImpactStored 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 fixRoute `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:
CVSS v3.1 score: 5.4