Vulnerability: Cross-site Scripting in CKEditor4 ### Affected packages
The vulnerability has been discovered in the core HTML processing module and may affect all plugins used by CKEditor 4.

### Impact
A potential vulnerability has been discovered in CKEditor 4 HTML processing core module. The vulnerability allowed to inject malformed HTML bypassing content sanitization, which could result in executing JavaScript code. It affects all users using the CKEditor 4 at version < 4.18.0.

### Patches
The problem has been recognized and patched. The fix will be available in version 4.18.0.

### For more information
Email us at security@cksource.com if you have any questions or comments about this advisory.

### Acknowledgements
The CKEditor 4 team would like to thank GHSL team member Kevin Backhouse ([@kevinbackhouse](https://github.com/kevinbackhouse)) for recognizing and reporting this vulnerability.
Vulnerability ID: GHSA-4fc4-4p5g-6w89
CWE: CWE-79
Severity score: 5.4
Project: https://github.com/ckeditor/ckeditor4
Published: 2022-03-16
File: core/htmldataprocessor.js
Commit message: Code refactoring.
Commit author: Tomasz Jakut

Vulnerable version (before fix):
 		htmlFilter.addRules( createBogusAndFillerRules( editor, 'html' ), { applyToAll: true } );  		editor.on( 'toHtml', function( evt ) { 			var evtData = evt.data, 				data = evtData.dataValue, 				fixBodyTag;  			// Before we start protecting markup, make sure there are no externally injected 			// protection keywords. 			data = removeReservedKeywords( data );  			// The source data is already HTML, but we need to clean 			// it up and apply the filter. 			data = protectSource( data, editor );  			// Protect content of textareas. (https://dev.ckeditor.com/ticket/9995) 			// Do this before protecting attributes to avoid breaking: 			// 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 );  			// Protect elements than can't be set inside a DIV. E.g. IE removes 			// style tags from innerHTML. (https://dev.ckeditor.com/ticket/3710)  			// 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 );  			var fixBin = evtData.context || editor.editable().getName(), 				isPre; 			data = el.getHtml().substr( 1 );  			// Restore shortly protected attribute names. 			data = data.replace( new RegExp( 'data-cke-' + CKEDITOR.rnd + '-', 'ig' ), '' );  			isPre && ( data = data.replace( /^<pre>|<\/pre>$/gi, '' ) );   	var protectSelfClosingRegex = /<cke:(param|embed)([^>]*?)\/?>(?!\s*<\/cke:\1)/gi;  	function protectAttributes( html ) { 		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 fullAttr; 			} ) + '>'; 	// * 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 unprotectRealComments( html ) { 		} ); 	}  	function protectSource( data, editor ) { 		var protectedHtml = [], 			protectRegexes = editor.config.protectedSource, 			store = editor._.dataStore || ( editor._.dataStore = { id: 1 } ), 			tempRegex = /<\!--\{cke_temp(comment)?\}(\d*?)-->/g;  		var regexes = [ 			// Script tags will also be forced to be protected, otherwise 		// 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 ) + '-->'; 		} );  		for ( var i = 0; i < regexes.length; i++ ) { 				} );  				// Avoid protecting over protected, e.g. /\{.*?\}/ 				return ( /cke_temp(comment)?/ ).test( match ) ? match : '<!--{cke_temp}' + ( protectedHtml.push( match ) - 1 ) + '-->'; 			} ); 		} 		data = data.replace( tempRegex, function( $, isComment, id ) { 			}; 		} 	} )(); } )();  /**

Fixed version (after fix):
 		htmlFilter.addRules( createBogusAndFillerRules( editor, 'html' ), { applyToAll: true } );  		editor.on( 'toHtml', function( evt ) { 			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, randomNumber );  			// The source data is already HTML, but we need to clean 			// it up and apply the filter. 			data = protectSource( data, editor, randomNumber );  			// Protect content of textareas. (https://dev.ckeditor.com/ticket/9995) 			// Do this before protecting attributes to avoid breaking: 			// 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, 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)  			// 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, randomNumber );  			var fixBin = evtData.context || editor.editable().getName(), 				isPre; 			data = el.getHtml().substr( 1 );  			// Restore shortly protected attribute names. 			data = data.replace( new RegExp( 'data-cke-' + randomNumber + '-', 'ig' ), '' );  			isPre && ( data = data.replace( /^<pre>|<\/pre>$/gi, '' ) );   	var protectSelfClosingRegex = /<cke:(param|embed)([^>]*?)\/?>(?!\s*<\/cke:\1)/gi;  	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-' + randomNumber + '-' + fullAttr;  				return fullAttr; 			} ) + '>'; 	// * 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, randomNumber ) { 		return html.replace( /([^a-z0-9<\-])(on\w{3,})(?!>)/gi, '$1data-cke-' + randomNumber + '-$2' ); 	}  	function unprotectRealComments( html ) { 		} ); 	}  	function protectSource( data, editor, randomNumber ) { 		var protectedHtml = [], 			protectRegexes = editor.config.protectedSource, 			store = editor._.dataStore || ( editor._.dataStore = { id: 1 } ), 			tempRegex = new RegExp('<\\!--\\{cke_temp_' + randomNumber + '(comment)?\\}(\\d*?)-->', 'g' );  		var regexes = [ 			// Script tags will also be forced to be protected, otherwise 		// 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_temp_' + randomNumber + 'comment}' + ( protectedHtml.push( match ) - 1 ) + '-->'; 		} );  		for ( var i = 0; i < regexes.length; i++ ) { 				} );  				// Avoid protecting over protected, e.g. /\{.*?\}/ 				return ( tempRegex ).test( match ) ? match : '<!--{cke_temp_' + randomNumber + '}' + 					( protectedHtml.push( match ) - 1 ) + '-->'; 			} ); 		} 		data = data.replace( tempRegex, function( $, isComment, id ) { 			}; 		} 	} )();  	function generateRandomNumber() { 		var cryptoApi = window.crypto || window.msCrypto;  		if ( cryptoApi ) { 			return cryptoApi.getRandomValues( new Uint32Array( 1 ) )[ 0 ]; 		}  		return Math.floor( Math.random() *  9000000000 + 1000000000 ); 	} } )();  /**

Patch:
patch: @@ -50,17 +50,18 @@
 		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:
@@ -70,7 +71,7 @@
 			// 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)
@@ -90,7 +91,7 @@
 
 			// 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;
@@ -110,7 +111,7 @@
 			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, '' ) );
 
@@ -838,13 +839,13 @@
 
 	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;
 			} ) + '>';
@@ -897,8 +898,8 @@
 	// * 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 ) {
@@ -917,11 +918,11 @@
 		} );
 	}
 
-	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
@@ -940,7 +941,7 @@
 		// 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++ ) {
@@ -951,7 +952,8 @@
 				} );
 
 				// 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 ) {
@@ -1107,6 +1109,16 @@
 			};
 		}
 	} )();
+
+	function generateRandomNumber() {
+		var cryptoApi = window.crypto || window.msCrypto;
+
+		if ( cryptoApi ) {
+			return cryptoApi.getRandomValues( new Uint32Array( 1 ) )[ 0 ];
+		}
+
+		return Math.floor( Math.random() *  9000000000 + 1000000000 );
+	}
 } )();
 
 /**
path: core/htmldataprocessor.js