CVE-2021-4107: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: yetiforcecrm is vulnerable to Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
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: 6.1
Published: 2021-12-14T15:15Z
Repository: https://github.com/yetiforcecompany/yetiforcecrm
Language: PHP
Files changed: config/version.php, modules/Vtiger/actions/Fields.php, public_html/layouts/resources/Fields.js
Commit date: 2021-12-13 15:18:13 +0100
Commit message: Added additional data validation in the wysiwyg editor when inputting data in the source.

Diff: diff --git a/config/version.php b/config/version.php
index 4b88d48449..fc47a12563 100644
--- a/config/version.php
+++ b/config/version.php
@@ -1,7 +1,7 @@
 <?php
 
 return [
-	'appVersion' => '6.3.3',
+	'appVersion' => '6.3.4',
 	'patchVersion' => '2021.12.13',
 	'lib_roundcube' => '0.2.3',
 ];
diff --git a/modules/Vtiger/actions/Fields.php b/modules/Vtiger/actions/Fields.php
index 76ecf4227d..1a2b650728 100644
--- a/modules/Vtiger/actions/Fields.php
+++ b/modules/Vtiger/actions/Fields.php
@@ -51,12 +51,13 @@ class Vtiger_Fields_Action extends \App\Controller\Action
 	{
 		parent::__construct();
 		$this->exposeMethod('getOwners');
 		$this->exposeMethod('getReference');
 		$this->exposeMethod('getUserRole');
-		$this->exposeMethod('verifyPhoneNumber');
 		$this->exposeMethod('findAddress');
+		$this->exposeMethod('validate');
+		$this->exposeMethod('verifyPhoneNumber');
 		$this->exposeMethod('verifyIsHolidayDate');
 		$this->exposeMethod('changeFavoriteOwner');
 	}
 
 	/**
@@ -265,6 +266,29 @@ class Vtiger_Fields_Action extends \App\Controller\Action
 		$message = \App\Language::translate($this->fieldModel->getFieldLabel(), $moduleName) . ': ' . \App\Language::translate($message, $moduleName);
 		$response = new Vtiger_Response();
 		$response->setResult(['result' => $result, 'message' => $message]);
 		$response->emit();
 	}
+
+	/**
+	 * Validate the field value.
+	 *
+	 * @param \App\Request $request
+	 *
+	 * @throws \App\Exceptions\NoPermitted
+	 */
+	public function validate(App\Request $request)
+	{
+		$fieldModel = Vtiger_Module_Model::getInstance($request->getModule())->getFieldByName($request->getByType('fieldName', 2));
+		if (!$fieldModel || !$fieldModel->isActiveField() || !$fieldModel->isViewEnabled()) {
+			throw new \App\Exceptions\NoPermitted('ERR_NO_PERMISSIONS_TO_FIELD', 406);
+		}
+		$recordModel = \Vtiger_Record_Model::getCleanInstance($fieldModel->getModuleName());
+		$fieldModel->getUITypeModel()->setValueFromRequest($request, $recordModel, 'fieldValue');
+		$response = new Vtiger_Response();
+		$response->setResult([
+			'raw' => $recordModel->get($fieldModel->getName()),
+			'display' => $recordModel->getDisplayValue($fieldModel->getName()),
+		]);
+		$response->emit();
+	}
 }
diff --git a/public_html/layouts/resources/Fields.js b/public_html/layouts/resources/Fields.js
index 4ae6daa9c1..655fc4957a 100644
--- a/public_html/layouts/resources/Fields.js
+++ b/public_html/layouts/resources/Fields.js
@@ -530,12 +530,11 @@ window.App.Fields = {
 			 * @param {HTMLElement|jQuery} element on which CkEditor has to be loaded
 			 * @param {Object} customConfig custom configurations for ckeditor
 			 */
 			loadEditor(element, customConfig) {
 				this.setElement(element);
-				const instance = this.getEditorInstanceFromName(),
-					self = this;
+				const instance = this.getEditorInstanceFromName();
 				let config = {
 					language: CONFIG.langKey,
 					allowedContent: true,
 					disableNativeSpellChecker: false,
 					extraAllowedContent: 'div{page-break-after*}',
@@ -544,16 +543,21 @@ window.App.Fields = {
 					enterMode: CKEDITOR.ENTER_BR,
 					shiftEnterMode: CKEDITOR.ENTER_P,
 					emojiEnabled: false,
 					mentionsEnabled: false,
 					on: {
-						instanceReady: function (evt) {
+						instanceReady: (evt) => {
 							evt.editor.on('blur', function () {
 								evt.editor.updateElement();
 							});
-							if (self.isModal && self.progressInstance) {
-								self.progressInstance.progressIndicator({ mode: 'hide' });
+							if (this.isModal && this.progressInstance) {
+								this.progressInstance.progressIndicator({ mode: 'hide' });
+							}
+						},
+						beforeCommandExec: (e) => {
+							if (e.editor.mode === 'source') {
+								return this.validate(element, e);
 							}
 						}
 					},
 					removePlugins: 'scayt',
 					extraPlugins:
@@ -739,10 +743,42 @@ window.App.Fields = {
 			 * @param {function} callback
 			 */
 			getMentionUsersData(opts, callback) {
 				App.Fields.Text.getMentionData(opts, callback, 'owners');
 			}
+
+			/**
+			 * Function to validate the field value
+			 * @param {jQuery} element
+			 * @param {object} e
+			 */
+			validate(element, e) {
+				let status = true;
+				AppConnector.request({
+					async: false,
+					data: {
+						module: element.closest('form').find('[name="module"]').val(),
+						action: 'Fields',
+						mode: 'validate',
+						fieldName: element.attr('name'),
+						fieldValue: element.val()
+					}
+				})
+					.done(function (data) {
+						element.val(data.result.raw);
+					})
+					.fail(function (error) {
+						app.showNotify({
+							type: 'error',
+							title: app.vtranslate('JS_ERROR'),
+							text: error
+						});
+						status = false;
+					});
+
+				return status;
+			}
 		},
 		/**
 		 * Completions class for contenteditable html element for records, users and emojis. Params can be passed in data-completions- of contenteditable element or as argument. Default params:
 		 * {
 		 		completionsCollection: {

Vulnerable code: 	'appVersion' => '6.3.3',
		$this->exposeMethod('verifyPhoneNumber');
				const instance = this.getEditorInstanceFromName(),
					self = this;
						instanceReady: function (evt) {
							if (self.isModal && self.progressInstance) {
								self.progressInstance.progressIndicator({ mode: 'hide' });
Fixed code: 	'appVersion' => '6.3.4',
		$this->exposeMethod('validate');
		$this->exposeMethod('verifyPhoneNumber');

	/**
	 * Validate the field value.
	 *
	 * @param \App\Request $request
	 *
	 * @throws \App\Exceptions\NoPermitted
	 */
	public function validate(App\Request $request)
	{
		$fieldModel = Vtiger_Module_Model::getInstance($request->getModule())->getFieldByName($request->getByType('fieldName', 2));
		if (!$fieldModel || !$fieldModel->isActiveField() || !$fieldModel->isViewEnabled()) {
			throw new \App\Exceptions\NoPermitted('ERR_NO_PERMISSIONS_TO_FIELD', 406);
		}
		$recordModel = \Vtiger_Record_Model::getCleanInstance($fieldModel->getModuleName());
		$fieldModel->getUITypeModel()->setValueFromRequest($request, $recordModel, 'fieldValue');
		$response = new Vtiger_Response();
		$response->setResult([
			'raw' => $recordModel->get($fieldModel->getName()),
			'display' => $recordModel->getDisplayValue($fieldModel->getName()),
		]);
		$response->emit();
	}
				const instance = this.getEditorInstanceFromName();
						instanceReady: (evt) => {
							if (this.isModal && this.progressInstance) {
								this.progressInstance.progressIndicator({ mode: 'hide' });
							}
						},
						beforeCommandExec: (e) => {
							if (e.editor.mode === 'source') {
								return this.validate(element, e);

			/**
			 * Function to validate the field value
			 * @param {jQuery} element
			 * @param {object} e
			 */
			validate(element, e) {
				let status = true;
				AppConnector.request({
					async: false,
					data: {
						module: element.closest('form').find('[name="module"]').val(),
						action: 'Fields',
						mode: 'validate',
						fieldName: element.attr('name'),
						fieldValue: element.val()
					}
				})
					.done(function (data) {
						element.val(data.result.raw);
					})
					.fail(function (error) {
						app.showNotify({
							type: 'error',
							title: app.vtranslate('JS_ERROR'),
							text: error
						});
						status = false;
					});

				return status;
			}