CVE-2021-4111 (PHP, published 2021-12-15T09:15Z)
Description: value: yetiforcecrm is vulnerable to Business Logic Errors
Severity: MEDIUM (CVSS v3 base score: 4.3)
Weakness: CWE-20 \u2014 Improper Input Validation
The product receives input or data, but it does
        not validate or incorrectly validates that the input has the
        properties that are required to process the data safely and
        correctly.
Repository: https://github.com/yetiforcecompany/yetiforcecrm
Files changed:
config/version.php
install/install_schema/data.sql
layouts/basic/modules/Vtiger/Edit/CurrenciesModal.tpl
layouts/basic/modules/Vtiger/Edit/Field/MultiCurrency.tpl
modules/Vtiger/models/Field.php
modules/Vtiger/uitypes/MultiCurrency.php
public_html/layouts/resources/validator/BaseValidator.js
public_html/layouts/resources/validator/FieldValidator.js
Commit message: Improved validation for multi-currency fields


Vulnerable code:
	'appVersion' => '6.3.2',
	'patchVersion' => '2021.12.12',
insert  into `vtiger_field`(`tabid`,`fieldid`,`columnname`,`tablename`,`generatedtype`,`uitype`,`fieldname`,`fieldlabel`,`readonly`,`presence`,`defaultvalue`,`maximumlength`,`sequence`,`block`,`displaytype`,`typeofdata`,`quickcreate`,`quickcreatesequence`,`info_type`,`masseditable`,`helpinfo`,`summaryfield`,`fieldparams`,`header_field`,`maxlengthtext`,`maxwidthcolumn`,`visible`,`tabindex`,`color`,`icon`) values (14,193,'unit_price','vtiger_products',1,360,'unit_price','Unit Price',0,0,'','65535',1,32,1,'V~O',2,3,'BAS',0,'',1,'',NULL,0,0,0,0,'',NULL);
insert  into `vtiger_field`(`tabid`,`fieldid`,`columnname`,`tablename`,`generatedtype`,`uitype`,`fieldname`,`fieldlabel`,`readonly`,`presence`,`defaultvalue`,`maximumlength`,`sequence`,`block`,`displaytype`,`typeofdata`,`quickcreate`,`quickcreatesequence`,`info_type`,`masseditable`,`helpinfo`,`summaryfield`,`fieldparams`,`header_field`,`maxlengthtext`,`maxwidthcolumn`,`visible`,`tabindex`,`color`,`icon`) values (14,2817,'purchase','vtiger_products',1,360,'purchase','FL_PURCHASE',0,2,'','65535',6,32,1,'V~O',2,3,'BAS',0,'',1,'',NULL,0,0,0,0,'',NULL);
							title="{\App\Language::translate('LBL_CLOSE')}">
												   id="cur_{$CURRENCY_ID}_check"
												   class="small float-right js-enable-currency"
												   data-js="change">
											   size="10" id="{$ITEM['name']}"
											   class="col-md-9 js-format-numer js-converted-price form-control"
											   data-validation-engine="validate[funcCall[Vtiger_Currency_Validator_Js.invokeValidation]]"
											   data-fieldinfo="{\App\Purifier::encodeHtml(\App\Json::encode($ITEM.fieldInfo))}"
											   data-js="value" disabled="disabled"/>
										<input name="currencies[{$CURRENCY_ID}]['rate']"
											   value="{App\Fields\Double::formatToDisplay($ITEM['conversionRate'], false)}"
											   class="col-md-9 js-conversion-rate form-control" type="text" size="10"
											   readonly="readonly" disabled="disabled">
												id="cur_reset{$CURRENCY_ID}"
												value="{\App\Language::translate('LBL_RESET',$MODULE_NAME)}"
												data-js="click">
											   class="js-base-currency" type="radio" disabled="disabled"
											   title="{\App\Language::translate('LBL_BASE_CURRENCY')}"
											   data-js="checked"/>
				title="{\App\Language::translate($FIELD_MODEL->getFieldLabel(), $MODULE_NAME)}" tabindex="{$TABINDEX}"
				{if $FIELD_MODEL->isEditableReadOnly()}disabled="disabled" {else}data-validation-engine="validate[{if $FIELD_MODEL->isMandatory() eq true} required,{/if}funcCall[Vtiger_Base_Validator_Js.invokeValidation]]" {/if}>
				$fieldPickListValues[$value] = \App\Language::translate($value, $this->getModuleName(),false,false);
					$fieldPickListValues[$fieldValue] = \App\Language::translate($fieldValue, $this->getModuleName(),false,false);
	 * Function returns Client Side Validators name.
	 * @return <Array> [name=>Name of the Validator, params=>Extra Parameters]
	public function getValidator()
			throw new \App\Exceptions\Security('ERR_ILLEGAL_FIELD_VALUE||' . $this->getFieldModel()->getFieldName() . '||' . $this->getFieldModel()->getModuleName() . '||' . $value, 406);
				throw new \App\Exceptions\Security('ERR_ILLEGAL_FIELD_VALUE||' . $this->getFieldModel()->getFieldName() . '||' . $this->getFieldModel()->getModuleName() . '||' . $id, 406);
				throw new \App\Exceptions\Security('ERR_ILLEGAL_FIELD_VALUE||' . $this->getFieldModel()->getFieldName() . '||' . $this->getFieldModel()->getModuleName() . '||' . $price, 406);
		$params = ['uitype' => 71, 'displaytype' => 1, 'typeofdata' => 'N~O', 'isEditableReadOnly' => false, 'maximumlength' => '99999999999999999'];
			$name = "currencies[$id]['value']";
			for (let i = 0; i < listOfValidators.length; i++) {
				for (key in specialValidators) {
					if (!specialValidators.hasOwnProperty(key)) {
					let specialValidator = specialValidators[key],
			if (maximumLength && strippedValue > parseFloat(maximumLength)) {
				errorInfo = app.vtranslate('JS_ERROR_MAX_VALUE');
				this.setError(errorInfo);
				return false;

Fixed code:
	'appVersion' => '6.3.3',
	'patchVersion' => '2021.12.13',
insert  into `vtiger_field`(`tabid`,`fieldid`,`columnname`,`tablename`,`generatedtype`,`uitype`,`fieldname`,`fieldlabel`,`readonly`,`presence`,`defaultvalue`,`maximumlength`,`sequence`,`block`,`displaytype`,`typeofdata`,`quickcreate`,`quickcreatesequence`,`info_type`,`masseditable`,`helpinfo`,`summaryfield`,`fieldparams`,`header_field`,`maxlengthtext`,`maxwidthcolumn`,`visible`,`tabindex`,`color`,`icon`) values (14,193,'unit_price','vtiger_products',1,360,'unit_price','Unit Price',0,0,'','0,99999999',1,32,1,'V~O',2,3,'BAS',0,'',1,'',NULL,0,0,0,0,'',NULL);
insert  into `vtiger_field`(`tabid`,`fieldid`,`columnname`,`tablename`,`generatedtype`,`uitype`,`fieldname`,`fieldlabel`,`readonly`,`presence`,`defaultvalue`,`maximumlength`,`sequence`,`block`,`displaytype`,`typeofdata`,`quickcreate`,`quickcreatesequence`,`info_type`,`masseditable`,`helpinfo`,`summaryfield`,`fieldparams`,`header_field`,`maxlengthtext`,`maxwidthcolumn`,`visible`,`tabindex`,`color`,`icon`) values (14,2817,'purchase','vtiger_products',1,360,'purchase','FL_PURCHASE',0,2,'','0,99999999',6,32,1,'V~O',2,3,'BAS',0,'',1,'',NULL,0,0,0,0,'',NULL);
						title="{\App\Language::translate('LBL_CLOSE')}">
												id="cur_{$CURRENCY_ID}_check"
												class="small float-right js-enable-currency"
												data-js="change">
											size="10" id="{$ITEM['name']}"
											class="col-md-9 js-format-numer js-converted-price form-control"
											data-validation-engine="validate[funcCall[Vtiger_Currency_Validator_Js.invokeValidation]]"
											data-fieldinfo="{\App\Purifier::encodeHtml(\App\Json::encode($ITEM.fieldInfo))}"
											data-js="value" disabled="disabled" />
										<input name="currencies[{$CURRENCY_ID}][rate]"
											value="{App\Fields\Double::formatToDisplay($ITEM['conversionRate'], false)}"
											class="col-md-9 js-conversion-rate form-control" type="text" size="10"
											readonly="readonly" disabled="disabled">
											id="cur_reset{$CURRENCY_ID}"
											value="{\App\Language::translate('LBL_RESET',$MODULE_NAME)}"
											data-js="click">
											class="js-base-currency" type="radio" disabled="disabled"
											title="{\App\Language::translate('LBL_BASE_CURRENCY')}"
											data-js="checked" />
	{assign var=FIELD_INFO value=\App\Purifier::encodeHtml(\App\Json::encode($FIELD_MODEL->getFieldInfo()))}
				title="{\App\Language::translate($FIELD_MODEL->getFieldLabel(), $MODULE_NAME)}" tabindex="{$TABINDEX}" data-fieldinfo="{$FIELD_INFO}"
				{if $FIELD_MODEL->isEditableReadOnly()} disabled="disabled" {else}data-validation-engine="validate[{if $FIELD_MODEL->isMandatory() eq true} required,{/if}funcCall[Vtiger_Base_Validator_Js.invokeValidation]]" {/if} {if !empty($SPECIAL_VALIDATOR)}data-validator='{\App\Purifier::encodeHtml(\App\Json::encode($SPECIAL_VALIDATOR))}' {/if}>
				$fieldPickListValues[$value] = \App\Language::translate($value, $this->getModuleName(), false, false);
					$fieldPickListValues[$fieldValue] = \App\Language::translate($fieldValue, $this->getModuleName(), false, false);
	 * Gets default validator.
	 * @return array
	public function getDefaultValidator(): array
	/**
	 * Function returns Client Side Validators name.
	 *
	 * @return array [name=>Name of the Validator, params=>Extra Parameters]
	 */
	public function getValidator()
	{
		return method_exists($this->getUITypeModel(), 'getValidator') ? $this->getUITypeModel()->getValidator() : $this->getDefaultValidator();
	}

	/**
	 * Get validator.
	 *
	 * @return array
	 */
	public function getValidator(): array
	{
		return [['name' => 'Currency']];
	}

			throw new \App\Exceptions\Security('ERR_ILLEGAL_FIELD_VALUE||' . $this->getFieldModel()->getName() . '||' . $this->getFieldModel()->getModuleName() . '||' . $value, 406);
				throw new \App\Exceptions\Security('ERR_ILLEGAL_FIELD_VALUE||' . $this->getFieldModel()->getName() . '||' . $this->getFieldModel()->getModuleName() . '||' . $id, 406);
				throw new \App\Exceptions\Security('ERR_ILLEGAL_FIELD_VALUE||' . $this->getFieldModel()->getName() . '||' . $this->getFieldModel()->getModuleName() . '||' . $price, 406);
			}
			if ($maximumLength = $this->getFieldModel()->get('maximumlength')) {
				[$minimumLength, $maximumLength] = false !== strpos($maximumLength, ',') ? explode(',', $maximumLength) : [-$maximumLength, $maximumLength];
				if ((float) $minimumLength > $price || (float) $maximumLength < $price) {
					throw new \App\Exceptions\Security('ERR_VALUE_IS_TOO_LONG||' . $this->getFieldModel()->getName() . '||' . $this->getFieldModel()->getModuleName() . "||{$maximumLength} < {$price} < {$minimumLength}", 406);
				}
		$params = ['uitype' => 71, 'displaytype' => 1, 'typeofdata' => 'N~O', 'isEditableReadOnly' => false, 'maximumlength' => $this->getFieldModel()->get('maximumlength')];
			$name = "currencies[$id][value]";
			for (let i in listOfValidators) {
				for (let s in specialValidators) {
					if (!specialValidators.hasOwnProperty(s)) {
					let specialValidator = specialValidators[s],
			if (maximumLength) {
				let ranges = maximumLength.split(',');
				if (
					(ranges.length === 2 && (strippedValue > parseFloat(ranges[1]) || strippedValue < parseFloat(ranges[0]))) ||
					(ranges.length === 1 && (strippedValue > parseFloat(ranges[0]) || strippedValue < 0))
				) {
					errorInfo = app.vtranslate('JS_ERROR_MAX_VALUE');
					this.setError(errorInfo);
					return false;
				}