VYPR
Moderate severityNVD Advisory· Published Feb 6, 2026· Updated Feb 9, 2026

SCEditor affected by DOM XSS via emoticon URL/HTML injection

CVE-2026-25581

Description

SCEditor is a lightweight WYSIWYG BBCode and XHTML editor. Prior to 3.2.1, if an attacker has the ability control configuration options passed to sceditor.create(), like emoticons, charset, etc. then it's possible for them to trigger an XSS attack due to lack of sanitisation of configuration options. This vulnerability is fixed in 3.2.1.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

SCEditor before 3.2.1 lacks sanitization of configuration options like charset, style, and emoticon URLs, enabling DOM XSS.

Vulnerability

Overview

SCEditor is a lightweight WYSIWYG BBCode and XHTML editor. Prior to version 3.2.1, the sceditor.create() function did not sanitize user-controlled configuration options such as charset, style, and emoticon URLs. An attacker who can control these options can inject arbitrary HTML and JavaScript into the editor's iframe, leading to a DOM-based cross-site scripting (XSS) attack [1][2].

Exploitation

Details

The vulnerability is triggered when an attacker supplies malicious values for configuration parameters. For example, the charset option was directly inserted into the iframe's HTML without validation, allowing injection of script tags. Similarly, the style option was not escaped, and emoticon URLs were not validated against allowed URI schemes [1]. The attacker must have the ability to influence the configuration object passed to options object passed to sceditor.create(), which could occur in applications that allow user-defined editor settings or through other injection points [4].

Impact

Successful exploitation allows an attacker to execute arbitrary JavaScript in the context of the victim's session. This can lead to data theft, session hijacking, defacement, or other malicious actions, depending on the application's trust level and the user's privileges [2][4].

Mitigation

The issue is fixed in SCEditor version 3.2.1. The fix includes proper validation and escaping of configuration options: charset is restricted to alphanumeric and hyphen characters, style is escaped via escape.entities(), and emoticon URLs are sanitized with escape.uriScheme() [1]. Users should upgrade to 3.2.1 or later. No workarounds are documented, but restricting the ability to pass arbitrary configuration options can reduce risk [3].

AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
sceditornpm
< 3.2.13.2.1

Affected products

2

Patches

1
5733aed4f0e2

Merge commit from fork

7 files changed · +87 44
  • src/lib/defaultCommands.js+10 2 modified
    @@ -213,6 +213,11 @@ var defaultCmds = {
     					html += '<div class="sceditor-color-column">';
     
     					column.split(',').forEach(function (color) {
    +						// Only allow named, #aaa, hsl(1.1 50% / 1), etc.
    +						if (!/^[\#a-z0-9\-\(\) \/%\.]+$/i.test(color)) {
    +							color = '';
    +						}
    +
     						html +=
     							'<a href="#" class="sceditor-color-option"' +
     							' style="background-color: ' + color + '"' +
    @@ -695,7 +700,8 @@ var defaultCmds = {
     
     				utils.each(emoticons, function (code, emoticon) {
     					dom.appendChild(line, dom.createElement('img', {
    -						src: emoticonsRoot + (emoticon.url || emoticon),
    +						src: escape.uriScheme(
    +							emoticonsRoot + (emoticon.url || emoticon)),
     						alt: code,
     						title: emoticon.tooltip || code
     					}));
    @@ -775,10 +781,12 @@ var defaultCmds = {
     			var editor = this;
     
     			defaultCmds.youtube._dropDown(editor, btn, function (id, time) {
    +				// Set sanatise to false as sanatisaion is handled by
    +				//  wysiwygEditorInsertHtml()
     				editor.wysiwygEditorInsertHtml(_tmpl('youtube', {
     					id: id,
     					time: time
    -				}));
    +				}, false, false));
     			});
     		},
     		tooltip: 'Insert a YouTube video'
    
  • src/lib/escape.js+29 26 modified
    @@ -1,13 +1,9 @@
    -// Must start with a valid scheme
    -// 		^
    -// Schemes that are considered safe
    -// 		(https?|s?ftp|mailto|spotify|skype|ssh|teamspeak|tel):|
    -// Relative schemes (//:) are considered safe
    -// 		(\\/\\/)|
    -// Image data URI's are considered safe
    -// 		data:image\\/(png|bmp|gif|p?jpe?g);
    -var VALID_SCHEME_REGEX =
    -	/^(https?|s?ftp|mailto|spotify|skype|ssh|teamspeak|tel):|(\/\/)|data:image\/(png|bmp|gif|p?jpe?g);/i;
    +// Regex used by DOMPurify to filter URLs. Might as well match here as otherwise
    +// URLs will be filtered out by DOMPurify anyway
    +var VALID_URI_REGEX = /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i;
    +// Safe image data URIs
    +var VALID_DATA_REGEX = /^data:image\/(png|bmp|gif|p?jpe?g);/i;
    +var WHITESPACE_REGEX = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g;
     
     /**
      * Escapes a string so it's safe to use in regex
    @@ -65,15 +61,15 @@ export function entities(str, noQuotes) {
      *
      * http
      * https
    - * sftp
    + * ftps
      * ftp
      * mailto
    - * spotify
    - * skype
    - * ssh
    - * teamspeak
      * tel
    - * //
    + * callto
    + * sms
    + * cid
    + * xmpp
    + * matrix
      * data:image/(png|jpeg|jpg|pjpeg|bmp|gif);
      *
      * **IMPORTANT**: This does not escape any HTML in a url, for
    @@ -85,20 +81,27 @@ export function entities(str, noQuotes) {
      */
     export function uriScheme(url) {
     	var	path,
    -		// If there is a : before a / then it has a scheme
    -		hasScheme = /^[^\/]*:/i,
     		location = window.location;
     
    -	// Has no scheme or a valid scheme
    -	if ((!url || !hasScheme.test(url)) || VALID_SCHEME_REGEX.test(url)) {
    +	// Match previous behaviour for empty or data: URIs
    +	if (!url || VALID_DATA_REGEX.test(url)) {
     		return url;
     	}
     
    -	path = location.pathname.split('/');
    -	path.pop();
    +	// Invalid scheme so make relative
    +	if (!VALID_URI_REGEX.test(url.replace(WHITESPACE_REGEX, ''))) {
    +		path = location.pathname.split('/');
    +		path.pop();
     
    -	return location.protocol + '//' +
    -		location.host +
    -		path.join('/') + '/' +
    -		url;
    +		url = location.protocol + '//' +
    +			location.host +
    +			path.join('/') + '/' +
    +			url;
    +
    +		if (!VALID_URI_REGEX.test(url.replace(WHITESPACE_REGEX, ''))) {
    +			return '';
    +		}
    +	}
    +
    +	return url;
     };
    
  • src/lib/SCEditor.js+12 6 modified
    @@ -571,17 +571,23 @@ export default function SCEditor(original, userOptions) {
     			options.height || dom.height(original)
     		);
     
    -		// Add ios to HTML so can apply CSS fix to only it
    -		var className = browser.ios ? ' ios' : '';
    +		if (!domPurify.isValidAttribute('link', 'href', options.style)) {
    +			options.style = '';
    +		}
    +
    +		if (!/^[a-z\-0-9 ]+$/i.test(options.charset)) {
    +			options.charset = 'UTF-8';
    +		}
     
     		wysiwygDocument = wysiwygEditor.contentDocument;
     		wysiwygDocument.open();
     		wysiwygDocument.write(_tmpl('html', {
    -			attrs: ' class="' + className + '"',
    +			// Add ios to HTML so can apply CSS fix to only it
    +			attrs: browser.ios ? ' class="ios"' : '',
     			spellcheck: options.spellcheck ? '' : 'spellcheck="false"',
     			charset: options.charset,
    -			style: options.style
    -		}));
    +			style: escape.entities(options.style)
    +		}, false, false));
     		wysiwygDocument.close();
     
     		wysiwygBody = wysiwygDocument.body;
    @@ -969,7 +975,7 @@ export default function SCEditor(original, userOptions) {
     			// Preload the emoticon
     			if (options.emoticonsEnabled) {
     				preLoadCache.push(dom.createElement('img', {
    -					src: root + (url.url || url)
    +					src: escape.uriScheme(root + (url.url || url))
     				}));
     			}
     		});
    
  • src/lib/templates.js+13 2 modified
    @@ -1,6 +1,6 @@
     import * as dom from './dom.js';
     import * as escape from './escape.js';
    -
    +import DOMPurify from 'dompurify';
     
     /**
      * HTML templates used by the editor and default commands
    @@ -92,18 +92,29 @@ var _templates = {
      * @param {string} name
      * @param {Object} [params]
      * @param {boolean} [createHtml]
    + * @param {boolean} [sanitize=true]
      * @returns {string|DocumentFragment}
      * @private
      */
    -export default function (name, params, createHtml) {
    +export default function (name, params, createHtml, sanitize) {
     	var template = _templates[name];
     
     	Object.keys(params).forEach(function (name) {
    +		// Default to sanitizing
    +		if (sanitize !== false) {
    +			params[name] = escape.entities(String(params[name]));
    +		}
    +
     		template = template.replace(
     			new RegExp(escape.regex('{' + name + '}'), 'g'), params[name]
     		);
     	});
     
    +	// Default to sanitizing
    +	if (sanitize !== false) {
    +		template = DOMPurify.sanitize(template, {ADD_ATTR: ['unselectable']});
    +	}
    +
     	if (createHtml) {
     		template = dom.parseHTML(template);
     	}
    
  • src/plugins/dragdrop.js+5 1 modified
    @@ -184,10 +184,14 @@
     
     			cover = container.appendChild(sceditor.dom.parseHTML(
     				'<div class="sceditor-dnd-cover" style="display: none">' +
    -					'<p>' + editor._('Drop files here') + '</p>' +
    +					'<p></p>' +
     				'</div>'
     			).firstChild);
     
    +			cover.firstChild.appendChild(
    +				document.createTextNode(editor._('Drop files here'))
    +			);
    +
     			container.addEventListener('dragover', handleDragOver);
     			container.addEventListener('dragleave', hideCover);
     			container.addEventListener('dragend', hideCover);
    
  • tests/unit/formats/bbcode.parser.js+2 2 modified
    @@ -966,7 +966,7 @@ QUnit.test('[img]', function (assert) {
     	);
     
     	assert.ok(
    -		!/src="jav/i.test(
    +		/src="jav&amp;/i.test(
     			this.parser.toHTML('[img]jav&#x0A;ascript:alert(' +
     				'String.fromCharCode(88,83,83))[/img]')
     		),
    @@ -1038,7 +1038,7 @@ QUnit.test('[url]', function (assert) {
     	);
     
     	assert.ok(
    -		!/href="jav/i.test(
    +		/href="jav&amp;/i.test(
     			this.parser.toHTML('[url]jav&#x0A;ascript:alert(' +
     				'String.fromCharCode(88,83,83))[/url]')
     		),
    
  • tests/unit/lib/escape.js+16 5 modified
    @@ -71,14 +71,13 @@ QUnit.test('uriScheme() - Valid schmes', function (assert) {
     		'http://localhost',
     		'https://example.com/test.html',
     		'ftp://localhost',
    -		'sftp://example.com/test/',
    +		'ftps://localhost',
     		'mailto:user@localhost',
    -		'spotify:xyz',
    -		'skype:xyz',
    -		'ssh:user@host.com:22',
    -		'teamspeak:12345',
     		'tel:12345',
    +		'tel:+12345',
     		'//www.example.com/test?id=123',
    +		'sms:12345',
    +		'sms:+1234',
     		'data:image/png;test',
     		'data:image/gif;test',
     		'data:image/jpg;test',
    @@ -106,7 +105,19 @@ QUnit.test('uriScheme() - Invalid schmes', function (assert) {
     	var urls = [
     		// eslint-disable-next-line no-script-url
     		'javascript:alert("XSS");',
    +		'sftp://example.com/test/',
    +		'skype:xyz',
    +		'spotify:xyz',
    +		'ssh:user@host.com:22',
    +		'teamspeak:12345',
    +		// eslint-disable-next-line no-script-url
    +		'javascript:alert("XSS");//test.com/hello.html',
    +		// eslint-disable-next-line no-script-url
    +		'javascript:alert("XSS http://example.com");',
     		'jav	ascript:alert(\'XSS\');',
    +		'jav\0ascript:alert(1)',
    +		'jav\nascript:alert(1)',
    +		'new://https://example.com',
     		'vbscript:msgbox("XSS")',
     		'data:application/javascript;alert("xss")'
     	];
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.