CVE-2022-24728: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: CKEditor4 is an open source what-you-see-is-what-you-get HTML editor. A vulnerability has been discovered in the core HTML processing module and may affect all plugins used by CKEditor 4 prior to version 4.18.0. The vulnerability allows someone to inject malformed HTML bypassing content sanitization, which could result in executing JavaScript code. This problem has been patched in version 4.18.0. There are currently no known workarounds.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 5.4
Published: 2022-03-16T16:15Z
Repository: https://github.com/ckeditor/ckeditor4
Language: JavaScript
Files changed: core/htmldataprocessor.js
Commit date: 2022-03-08 08:46:07 +0100
Commit message: Code refactoring.

Diff: diff --git a/core/htmldataprocessor.js b/core/htmldataprocessor.js
index a57e4b53d6..9c96963160 100644
--- a/core/htmldataprocessor.js
+++ b/core/htmldataprocessor.js
@@ -48,31 +48,32 @@
 		htmlFilter.addRules( defaultHtmlFilterRulesEditableOnly );
 		htmlFilter.addRules( defaultHtmlFilterRulesForAll, { applyToAll: true } );
 		htmlFilter.addRules( createBogusAndFillerRules( editor, 'html' ), { applyToAll: true } );
 
 		editor.on( 'toHtml', function( evt ) {
-			var evtData = evt.data,
+			var randomNumber = generateRandomNumber(),
+				evtData = evt.data,
 				data = evtData.dataValue,
 				fixBodyTag;
 
 			// Before we start protecting markup, make sure there are no externally injected
 			// protection keywords.
-			data = removeReservedKeywords( data );
+			data = removeReservedKeywords( data, randomNumber );
 
 			// The source data is already HTML, but we need to clean
 			// it up and apply the filter.
-			data = protectSource( data, editor );
+			data = protectSource( data, editor, randomNumber );
 
 			// Protect content of textareas. (https://dev.ckeditor.com/ticket/9995)
 			// Do this before protecting attributes to avoid breaking:
 			// <textarea><img src="..." /></textarea>
 			data = protectElements( data, protectTextareaRegex );
 
 			// Before anything, we must protect the URL attributes as the
 			// browser may changing them when setting the innerHTML later in
 			// the code.
-			data = protectAttributes( data );
+			data = protectAttributes( data, randomNumber );
 
 			// Protect elements than can't be set inside a DIV. E.g. IE removes
 			// style tags from innerHTML. (https://dev.ckeditor.com/ticket/3710)
 			data = protectElements( data, protectElementsRegex );
 
@@ -88,11 +89,11 @@
 			// eat it up. (https://dev.ckeditor.com/ticket/5789)
 			data = protectPreFormatted( data );
 
 			// There are attributes which may execute JavaScript code inside fixBin.
 			// Encode them greedily. They will be unprotected right after getting HTML from fixBin. (https://dev.ckeditor.com/ticket/10)
-			data = protectInsecureAttributes( data );
+			data = protectInsecureAttributes( data, randomNumber );
 
 			var fixBin = evtData.context || editor.editable().getName(),
 				isPre;
 
 			// Old IEs loose formats when load html into <pre>.
@@ -108,11 +109,11 @@
 			// Add fake character to workaround IE comments bug. (https://dev.ckeditor.com/ticket/3801)
 			el.setHtml( 'a' + data );
 			data = el.getHtml().substr( 1 );
 
 			// Restore shortly protected attribute names.
-			data = data.replace( new RegExp( 'data-cke-' + CKEDITOR.rnd + '-', 'ig' ), '' );
+			data = data.replace( new RegExp( 'data-cke-' + randomNumber + '-', 'ig' ), '' );
 
 			isPre && ( data = data.replace( /^<pre>|<\/pre>$/gi, '' ) );
 
 			// Unprotect "some" of the protected elements at this point.
 			data = unprotectElementNames( data );
@@ -836,17 +837,17 @@
 	var protectElementNamesRegex = /(<\/?)((?:object|embed|param|html|body|head|title)([\s][^>]*)?>)/gi,
 		unprotectElementNamesRegex = /(<\/?)cke:((?:html|body|head|title)[^>]*>)/gi;
 
 	var protectSelfClosingRegex = /<cke:(param|embed)([^>]*?)\/?>(?!\s*<\/cke:\1)/gi;
 
-	function protectAttributes( html ) {
+	function protectAttributes( html, randomNumber ) {
 		return html.replace( protectElementRegex, function( element, tag, attributes ) {
 			return '<' + tag + attributes.replace( protectAttributeRegex, function( fullAttr, attrName ) {
 				// Avoid corrupting the inline event attributes (https://dev.ckeditor.com/ticket/7243).
 				// We should not rewrite the existed protected attributes, e.g. clipboard content from editor. (https://dev.ckeditor.com/ticket/5218)
 				if ( protectAttributeNameRegex.test( attrName ) && attributes.indexOf( 'data-cke-saved-' + attrName ) == -1 )
-					return ' data-cke-saved-' + fullAttr + ' data-cke-' + CKEDITOR.rnd + '-' + fullAttr;
+					return ' data-cke-saved-' + fullAttr + ' data-cke-' + randomNumber + '-' + fullAttr;
 
 				return fullAttr;
 			} ) + '>';
 		} );
 	}
@@ -895,12 +896,12 @@
 
 	// Replace all "on\w{3,}" strings which are not:
 	// * opening tags - e.g. `<onfoo`,
 	// * closing tags - e.g. </onfoo> (tested in "false positive 1"),
 	// * part of other attribute - e.g. `data-onfoo` or `fonfoo`.
-	function protectInsecureAttributes( html ) {
-		return html.replace( /([^a-z0-9<\-])(on\w{3,})(?!>)/gi, '$1data-cke-' + CKEDITOR.rnd + '-$2' );
+	function protectInsecureAttributes( html, randomNumber ) {
+		return html.replace( /([^a-z0-9<\-])(on\w{3,})(?!>)/gi, '$1data-cke-' + randomNumber + '-$2' );
 	}
 
 	function unprotectRealComments( html ) {
 		return html.replace( /<!--\{cke_protected\}\{C\}([\s\S]+?)-->/g, function( match, data ) {
 			return decodeURIComponent( data );
@@ -915,15 +916,15 @@
 		} ).replace( /\{cke_protected_(\d+)\}/g, function( match, id ) {
 			return store && store[ id ] || '';
 		} );
 	}
 
-	function protectSource( data, editor ) {
+	function protectSource( data, editor, randomNumber ) {
 		var protectedHtml = [],
 			protectRegexes = editor.config.protectedSource,
 			store = editor._.dataStore || ( editor._.dataStore = { id: 1 } ),
-			tempRegex = /<\!--\{cke_temp(comment)?\}(\d*?)-->/g;
+			tempRegex = new RegExp('<\\!--\\{cke_temp_' + randomNumber + '(comment)?\\}(\\d*?)-->', 'g' );
 
 		var regexes = [
 			// Script tags will also be forced to be protected, otherwise
 			// IE will execute them.
 			( /<script[\s\S]*?(<\/script>|$)/gi ),
@@ -938,22 +939,23 @@
 		// First of any other protection, we must protect all comments
 		// to avoid loosing them (of course, IE related).
 		// Note that we use a different tag for comments, as we need to
 		// transform them when applying filters.
 		data = data.replace( ( /<!--[\s\S]*?-->/g ), function( match ) {
-			return '<!--{cke_tempcomment}' + ( protectedHtml.push( match ) - 1 ) + '-->';
+			return '<!--{cke_temp_' + randomNumber + 'comment}' + ( protectedHtml.push( match ) - 1 ) + '-->';
 		} );
 
 		for ( var i = 0; i < regexes.length; i++ ) {
 			data = data.replace( regexes[ i ], function( match ) {
 				match = match.replace( tempRegex, // There could be protected source inside another one. (https://dev.ckeditor.com/ticket/3869).
 				function( $, isComment, id ) {
 					return protectedHtml[ id ];
 				} );
 
 				// Avoid protecting over protected, e.g. /\{.*?\}/
-				return ( /cke_temp(comment)?/ ).test( match ) ? match : '<!--{cke_temp}' + ( protectedHtml.push( match ) - 1 ) + '-->';
+				return ( tempRegex ).test( match ) ? match : '<!--{cke_temp_' + randomNumber + '}' +
+					( protectedHtml.push( match ) - 1 ) + '-->';
 			} );
 		}
 		data = data.replace( tempRegex, function( $, isComment, id ) {
 			return '<!--' + protectedSourceMarker +
 				( isComment ? '{C}' : '' ) +
@@ -1105,10 +1107,20 @@
 				hex: '&#x0*' + hex + ';?',
 				entity: entities[ character ]
 			};
 		}
 	} )();
+
+	function generateRandomNumber() {
+		var cryptoApi = window.crypto || window.msCrypto;
+
+		if ( cryptoApi ) {
+			return cryptoApi.getRandomValues( new Uint32Array( 1 ) )[ 0 ];
+		}
+
+		return Math.floor( Math.random() *  9000000000 + 1000000000 );
+	}
 } )();
 
 /**
  * Whether a filler text (non-breaking space entity &mdash; `&nbsp;`) will be
  * inserted into empty block elements in HTML output.

Vulnerable code: 			var evtData = evt.data,
			data = removeReservedKeywords( data );
			data = protectSource( data, editor );
			data = protectAttributes( data );
			data = protectInsecureAttributes( data );
			data = data.replace( new RegExp( 'data-cke-' + CKEDITOR.rnd + '-', 'ig' ), '' );
	function protectAttributes( html ) {
					return ' data-cke-saved-' + fullAttr + ' data-cke-' + CKEDITOR.rnd + '-' + fullAttr;
	function protectInsecureAttributes( html ) {
		return html.replace( /([^a-z0-9<\-])(on\w{3,})(?!>)/gi, '$1data-cke-' + CKEDITOR.rnd + '-$2' );
	function protectSource( data, editor ) {
			tempRegex = /<\!--\{cke_temp(comment)?\}(\d*?)-->/g;
			return '<!--{cke_tempcomment}' + ( protectedHtml.push( match ) - 1 ) + '-->';
				return ( /cke_temp(comment)?/ ).test( match ) ? match : '<!--{cke_temp}' + ( protectedHtml.push( match ) - 1 ) + '-->';
Fixed code: 			var randomNumber = generateRandomNumber(),
				evtData = evt.data,
			data = removeReservedKeywords( data, randomNumber );
			data = protectSource( data, editor, randomNumber );
			data = protectAttributes( data, randomNumber );
			data = protectInsecureAttributes( data, randomNumber );
			data = data.replace( new RegExp( 'data-cke-' + randomNumber + '-', 'ig' ), '' );
	function protectAttributes( html, randomNumber ) {
					return ' data-cke-saved-' + fullAttr + ' data-cke-' + randomNumber + '-' + fullAttr;
	function protectInsecureAttributes( html, randomNumber ) {
		return html.replace( /([^a-z0-9<\-])(on\w{3,})(?!>)/gi, '$1data-cke-' + randomNumber + '-$2' );
	function protectSource( data, editor, randomNumber ) {
			tempRegex = new RegExp('<\\!--\\{cke_temp_' + randomNumber + '(comment)?\\}(\\d*?)-->', 'g' );
			return '<!--{cke_temp_' + randomNumber + 'comment}' + ( protectedHtml.push( match ) - 1 ) + '-->';
				return ( tempRegex ).test( match ) ? match : '<!--{cke_temp_' + randomNumber + '}' +
					( protectedHtml.push( match ) - 1 ) + '-->';

	function generateRandomNumber() {
		var cryptoApi = window.crypto || window.msCrypto;

		if ( cryptoApi ) {
			return cryptoApi.getRandomValues( new Uint32Array( 1 ) )[ 0 ];
		}

		return Math.floor( Math.random() *  9000000000 + 1000000000 );
	}