Jodit has incomplete javascript: scheme normalization in sanitizeHTMLElement href check that allows link XSS
### 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 = '
click me';
// editor.value getter returns the href unchanged:
//
click me
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: `

` 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