CVE-2022-1411 (PHP, published 2022-05-05T11:15Z)
Description: value: Unrestructed file upload in GitHub repository yetiforcecompany/yetiforcecrm prior to 6.4.0. Attacker can send malicious files to the victims is able to retrieve the stored data from the web application without that data being made safe to render in the browser and steals victim's cookie leads to account takeover.
Severity: MEDIUM (CVSS v3 base score: 6.1)
Weakness: CWE-434 \u2014 Unrestricted Upload of File with Dangerous Type
The product allows the upload or transfer of dangerous file types that are automatically processed within its environment.
Repository: https://github.com/yetiforcecompany/yetiforcecrm
Files changed:
app/Fields/File.php
app/Validator.php
config/version.php
modules/Vtiger/actions/Fields.php
public_html/layouts/resources/libraries/ckeditor/base64image/dialogs/dialog.js
public_html/layouts/resources/libraries/ckeditor/base64image/plugin.js
Commit message: Added validation to pasted files to the wysiwyg editor


Vulnerable code:
		$result = explode(',', $contents, 2);
		$contentType = $isBase64 = false;
		if (2 === \count($result)) {
			[$metadata, $data] = $result;
			foreach (explode(';', $metadata) as $cur) {
				if ('base64' === $cur) {
					$isBase64 = true;
				} elseif ('data:' === substr($cur, 0, 5)) {
					$contentType = str_replace('data:', '', $cur);
				}
			}
		} else {
			$data = $result[0];
		}
		$data = rawurldecode($data);
		$rawData = $isBase64 ? base64_decode($data) : $data;
		if (\strlen($rawData) < 12) {
			Log::error('Incorrect content value: ' . $contents, __CLASS__);
			return false;
		}
		$fileInstance = static::loadFromContent($rawData, false, array_merge($param, ['mimeType' => $contentType]));
			'crmid' => 0
	'appVersion' => '6.3.220',
	public function getOwners(App\Request $request)
	public function getUserRole(App\Request $request)
	public function getReference(App\Request $request)
	public function verifyPhoneNumber(App\Request $request)
	public function findAddress(App\Request $request)
	public function changeFavoriteOwner(App\Request $request)
	public function validateForField(App\Request $request)
	public function validateByMode(App\Request $request)
/* {[The file is published on the basis of YetiForce Public License 3.0 that can be found in the following directory: licenses/LicenseEN.txt or yetiforce.com]} */
	var t = null,
		imgScal = 1,
		lock = true;
		var r = false,
				var n = document.createElement('input');
	var isFReaderSupported = fileSupport();
		var i = new Image();
		imgPreview.getElement().setHtml('Loading...');
				t.setValueOf('tab-properties', 'width', this.width);
				t.setValueOf('tab-properties', 'height', this.height);
				imgScal = 1;
				if (this.height > 0 && this.width > 0) imgScal = this.width / this.height;
				if (imgScal <= 0) imgScal = 1;
				var p = imgPreview.getElement().$;
			var fileI = t.getContentElement('tab-source', 'file');
			var n = null;
			try {
				n = fileI.getInputElement().$;
			} catch (e) {
				n = null;
			}
			if (n && 'files' in n && n.files && n.files.length > 0 && n.files[0]) {
				if ('type' in n.files[0] && !n.files[0].type.match('image.*')) return;
				if (!FileReader) return;
				imgPreview.getElement().setHtml('Loading...');
				var fr = new FileReader();
				fr.onload = (function (f) {
					return function (e) {
						imgPreview.getElement().setHtml('');
						imagePreviewLoad(e.target.result);
					};
				})(n.files[0]);
				fr.onerror = function () {
				};
				fr.onabort = function () {
				};
				fr.readAsDataURL(n.files[0]);
		var o = {
			w: t.getContentElement('tab-properties', 'width').getValue(),
			h: t.getContentElement('tab-properties', 'height').getValue(),
		var o = getImageDimensions();
		var u = 'px';
			o.h = Math.round(o.w / imgScal);
			o.w = Math.round(o.h * imgScal);
		t.getContentElement('tab-properties', 'width').setValue(o.w),
			t.getContentElement('tab-properties', 'height').setValue(o.h);
		var v = elem.getValue(),
		var sourceElements = [
						onChange: function () {
							imagePreview('file');
			(t = this), (orgWidth = null), (orgHeight = null), (imgScal = 1), (lock = true);
			t.setValueOf('tab-properties', 'lock', lock);
			t.setValueOf('tab-properties', 'vmargin', '0');
			t.setValueOf('tab-properties', 'hmargin', '0');
			t.setValueOf('tab-properties', 'border', '0');
			t.setValueOf('tab-properties', 'align', 'none');
					t.setValueOf('tab-properties', 'width', orgWidth);
					t.setValueOf('tab-properties', 'height', orgHeight);
					imgScal = 1;
					if (!isNaN(orgWidth) && !isNaN(orgHeight) && orgHeight > 0 && orgWidth > 0) imgScal = orgWidth / orgHeight;
					if (imgScal <= 0) imgScal = 1;
					t.setValueOf('tab-properties', 'alt', selectedImg.getAttribute('alt'));
					t.setValueOf('tab-properties', 'hmargin', selectedImg.getAttribute('hspace'));
					t.setValueOf('tab-properties', 'vmargin', selectedImg.getAttribute('vspace'));
					t.setValueOf('tab-properties', 'border', selectedImg.getAttribute('border'));
							t.setValueOf('tab-properties', 'align', 'top');
							t.setValueOf('tab-properties', 'align', 'bottom');
							t.setValueOf('tab-properties', 'align', 'left');
							t.setValueOf('tab-properties', 'align', 'right');
				t.selectPage('tab-properties');
			var src = '';
			/* selected image or new image */
			if (selectedImg) var newImg = selectedImg;
			else var newImg = editor.document.createElement('img');
			newImg.setAttribute('src', src);
			src = null;
			/* Set attributes */
			newImg.setAttribute('alt', t.getValueOf('tab-properties', 'alt').replace(/^\s+/, '').replace(/\s+$/, ''));
			var attr = {
					width: ['width', 'width:#;', 'integer', 1],
					height: ['height', 'height:#;', 'integer', 1],
					vmargin: ['vspace', 'margin-top:#;margin-bottom:#;', 'integer', 0],
					hmargin: ['hspace', 'margin-left:#;margin-right:#;', 'integer', 0],
					align: ['align', ''],
					border: ['border', 'border:# solid black;', 'integer', 0]
				},
				css = [],
				value,
				cssvalue,
				attrvalue,
				k;
			for (k in attr) {
				value = t.getValueOf('tab-properties', k);
				attrvalue = value;
				cssvalue = value;
				unit = 'px';

				if (k == 'align') {
					switch (value) {
						case 'top':
						case 'bottom':
							attr[k][1] = 'vertical-align:#;';
							break;
						case 'left':
						case 'right':
							attr[k][1] = 'float:#;';
							break;
						default:
							value = null;
							break;
				}
				if (attr[k][2] == 'integer') {
					if (value.indexOf('%') >= 0) unit = '%';
					value = parseInt(value, 10);
					if (isNaN(value)) value = null;
					else if (value < attr[k][3]) value = null;
					if (value != null) {
						if (unit == '%') {
							attrvalue = value + '%';
							cssvalue = value + '%';
						} else {
							attrvalue = value;
							cssvalue = value + 'px';
				}
				if (value != null) {
					newImg.setAttribute(attr[k][0], attrvalue);
					css.push(attr[k][1].replace(/#/g, cssvalue));
			}
			if (css.length > 0) newImg.setAttribute('style', css.join(''));
			/* Insert new image */
			if (!selectedImg) editor.insertElement(newImg);
			/* Resize image */
			if (editor.plugins.imageresize) editor.plugins.imageresize.resize(editor, newImg, 800, 800);
/* {[The file is published on the basis of YetiForce Public License 3.0 that can be found in the following directory: licenses/LicenseEN.txt or yetiforce.com]} */

function initPasteEvent(editorInstance) {
	if (editorInstance.addFeature) {
		editorInstance.addFeature({
			allowedContent: 'img[alt,id,!src]{width,height};'
		});
	}

	editorInstance.on('contentDom', function () {
		var editableElement = editorInstance.editable ? editorInstance.editable() : editorInstance.document;
		editableElement.on('paste', onPaste, null, { editor: editorInstance });
	});
}
function onPaste(event) {
	var editor = event.listenerData && event.listenerData.editor;
	var $event = event.data.$;
	var clipboardData = $event.clipboardData;
	var found = false;
	var imageType = /^image/;

	if (!clipboardData) {
		return;
	}
	return Array.prototype.forEach.call(clipboardData.types, function (type, i) {
		if (found) {
			return;
		}
		if (type.match(imageType) || clipboardData.items[i].type.match(imageType)) {
			readImageAsBase64(clipboardData.items[i], editor);
			return (found = true);
		}
	});
}

function readImageAsBase64(item, editor) {
	if (!item || typeof item.getAsFile !== 'function') {
		return;
	}
	var file = item.getAsFile();
	var reader = new FileReader();
	reader.onload = function (evt) {
		var element = editor.document.createElement('img', {
			attributes: {
				src: evt.target.result
			}
		});
		setTimeout(function () {
			editor.insertElement(element);
		}, 10);
	};
	reader.readAsDataURL(file);
}
	init: function (editorInstance) {
		initPasteEvent(editorInstance);
		var pluginName = 'base64image-dialog';
		editorInstance.ui.addToolbarGroup('base64image', 'insert');
		editorInstance.ui.addButton('base64image', {
			label: editorInstance.lang.common.image,
		editorInstance.addCommand(
		editorInstance.on('doubleclick', function (evt) {
				editorInstance.getSelection().selectElement(evt.data.element);
		if (editorInstance.addMenuItem) {
			editorInstance.addMenuGroup('imageToBase64Group');
			editorInstance.addMenuItem('imageToBase64Item', {
				label: editorInstance.lang.common.image,
		if (editorInstance.contextMenu) {
			editorInstance.contextMenu.addListener(function (element, selection) {
					editorInstance.getSelection().selectElement(element);

Fixed code:
	/**
	 * Load file instance from base string.
	 *
	 * @param string $contents
	 * @param array  $param
	 *
	 * @return \self|null
	 */
	public static function loadFromBase(string $contents, array $param = []): ?self
	{
		$result = explode(',', $contents, 2);
		$contentType = $isBase64 = false;
		if (2 === \count($result)) {
			[$metadata, $data] = $result;
			foreach (explode(';', $metadata) as $cur) {
				if ('base64' === $cur) {
					$isBase64 = true;
				} elseif ('data:' === substr($cur, 0, 5)) {
					$contentType = str_replace('data:', '', $cur);
				}
			}
		} else {
			$data = $result[0];
		}
		$data = rawurldecode($data);
		$rawData = $isBase64 ? base64_decode($data) : $data;
		if (\strlen($rawData) < 12) {
			Log::error('Incorrect content value: ' . $contents, __CLASS__);
			return null;
		}
		return static::loadFromContent($rawData, false, array_merge($param, ['mimeType' => $contentType]));
	}

		$fileInstance = static::loadFromBase($contents, $param);
			'crmid' => 0,

	/**
	 * Check base64.
	 *
	 * @param string $input
	 *
	 * @return bool
	 */
	public static function base64(string $input): bool
	{
		if (empty($input)) {
			return false;
		}
		$explode = explode(',', $input);
		return 2 === \count($explode) && 1 === preg_match('%^[a-zA-Z0-9/+]*={0,2}$%', $explode[1]);
	}
	'appVersion' => '6.3.221',
		$this->exposeMethod('validateFile');
	public function getOwners(App\Request $request): void
	public function getUserRole(App\Request $request): void
	public function getReference(App\Request $request): void
	public function verifyPhoneNumber(App\Request $request): void
	public function findAddress(App\Request $request): void
	public function changeFavoriteOwner(App\Request $request): void
	public function validateForField(App\Request $request): void
	public function validateByMode(App\Request $request): void

	/**
	 * Validate file.
	 *
	 * @param App\Request $request
	 *
	 * @return void
	 */
	public function validateFile(App\Request $request): void
	{
		$validate = false;
		if ($request->has('base64')) {
			$fileInstance = \App\Fields\File::loadFromBase($request->getByType('base64', 'base64'), ['validateAllowedFormat' => 'image']);
			if ($fileInstance && $fileInstance->validate()) {
				$validate = true;
			} else {
				$validateError = $fileInstance->validateError;
			}
		}
		$response = new Vtiger_Response();
		$response->setResult([
			'validate' => $validate,
			'validateError' => $validateError ?? null,
		]);
		$response->emit();
	}
/* {[The file is published on the basis of YetiForce Public License 5.0 that can be found in the following directory: licenses/LicenseEN.txt or yetiforce.com]} */
'use strict';
	let self = null,
		sourceElements = [],
		imgScale = 1,
		lock = true,
		maxUploadSize = CONFIG['maxUploadLimit'];
		let r = false,
				let n = document.createElement('input');
	let isFReaderSupported = fileSupport();
		let i = new Image();
		$(imgPreview.getElement().$).progressIndicator({ position: 'html' });
				self.setValueOf('tab-properties', 'width', this.width);
				self.setValueOf('tab-properties', 'height', this.height);
				imgScale = 1;
				if (this.height > 0 && this.width > 0) imgScale = this.width / this.height;
				if (imgScale <= 0) imgScale = 1;
				let p = imgPreview.getElement().$;
			$(imgPreview.getElement().$).progressIndicator({ position: 'html' });
			readImageAsBase64()
				.done(function (base) {
					imagePreviewLoad(base);
				})
				.fail(function () {
				});
		}
	}

	function readImageAsBase64() {
		const aDeferred = jQuery.Deferred();
		let fileI = self.getContentElement('tab-source', 'file'),
			n = null;
		try {
			n = fileI.getInputElement().$;
		} catch (e) {
			n = null;
		}
		if (n && 'files' in n && n.files && n.files.length > 0 && n.files[0]) {
			if (('type' in n.files[0] && !n.files[0].type.match('image.*')) || !FileReader) {
				aDeferred.reject();
				return aDeferred.promise();
			}
			if (n.files[0].size > maxUploadSize) {
				app.showNotify({
					text: app.vtranslate('JS_UPLOADED_FILE_SIZE_EXCEEDS'),
					type: 'error'
				});
				aDeferred.reject();
				return aDeferred.promise();
			let fr = new FileReader();
			fr.onload = (function (f) {
				return function (e) {
					aDeferred.resolve(e.target.result);
				};
			})(n.files[0]);
			fr.onerror = function () {
				aDeferred.reject();
			};
			fr.onabort = function () {
				aDeferred.reject();
			};
			fr.readAsDataURL(n.files[0]);
		return aDeferred.promise();
		let o = {
			w: self.getContentElement('tab-properties', 'width').getValue(),
			h: self.getContentElement('tab-properties', 'height').getValue(),
		let o = getImageDimensions();
		let u = 'px';
			o.h = Math.round(o.w / imgScale);
			o.w = Math.round(o.h * imgScale);
		self.getContentElement('tab-properties', 'width').setValue(o.w),
			self.getContentElement('tab-properties', 'height').setValue(o.h);
		let v = elem.getValue(),
	function validateFile() {
		const fieldInfo = $(editor.element.$).data('fieldinfo');
		let length = editor.getData().length,
			selectedImg = editor.getSelection();
		if (selectedImg) selectedImg = selectedImg.getSelectedElement();
		if (!selectedImg || selectedImg.getName() !== 'img') selectedImg = null;
		if (selectedImg) {
			length = length - selectedImg.getOuterHtml().length;
		}
		const aDeferred = jQuery.Deferred();
		readImageAsBase64()
			.done((base) => {
				length += base.length;
				if (length > fieldInfo['maximumlength']) {
					app.showNotify({
						text: app.vtranslate('JS_MAXIMUM_TEXT_SIZE_IN_BYTES') + ' ' + fieldInfo['maximumlength'],
						type: 'error'
					});
				}
				AppConnector.request({
					module: app.getModuleName(),
					action: 'Fields',
					mode: 'validateFile',
					fieldName: fieldInfo['name'],
					base64: base
				})
					.done((data) => {
						if (data.result.validate) {
							aDeferred.resolve();
						} else {
							app.showNotify({
								text: data.result.validateError,
								type: 'error'
							});
							aDeferred.reject();
						}
					})
					.fail(function () {
						aDeferred.resolve();
					});
			})
			.fail(() => {
				aDeferred.reject();
			});
		return aDeferred.promise();
	}

		sourceElements = [
						size: maxUploadSize,
						onChange: function (a) {
							validateFile()
								.done(() => {
									imagePreview('file');
								})
								.fail(function () {
									self.getContentElement('tab-source', 'file').getInputElement().$.value = null;
									imgPreview.getElement().setHtml('');
								});
			this.getContentElement('tab-source', 'file')
				.getInputElement()
				.$.setAttribute('accept', 'image/jpeg, image/png, image/gif');
			(self = this), (orgWidth = null), (orgHeight = null), (imgScale = 1), (lock = true);
			self.setValueOf('tab-properties', 'lock', lock);
			self.setValueOf('tab-properties', 'vmargin', '0');
			self.setValueOf('tab-properties', 'hmargin', '0');
			self.setValueOf('tab-properties', 'border', '0');
			self.setValueOf('tab-properties', 'align', 'none');
					self.setValueOf('tab-properties', 'width', orgWidth);
					self.setValueOf('tab-properties', 'height', orgHeight);
					imgScale = 1;
					if (!isNaN(orgWidth) && !isNaN(orgHeight) && orgHeight > 0 && orgWidth > 0) imgScale = orgWidth / orgHeight;
					if (imgScale <= 0) imgScale = 1;
					self.setValueOf('tab-properties', 'alt', selectedImg.getAttribute('alt'));
					self.setValueOf('tab-properties', 'hmargin', selectedImg.getAttribute('hspace'));
					self.setValueOf('tab-properties', 'vmargin', selectedImg.getAttribute('vspace'));
					self.setValueOf('tab-properties', 'border', selectedImg.getAttribute('border'));
							self.setValueOf('tab-properties', 'align', 'top');
							self.setValueOf('tab-properties', 'align', 'bottom');
							self.setValueOf('tab-properties', 'align', 'left');
							self.setValueOf('tab-properties', 'align', 'right');
				self.selectPage('tab-properties');
			let src = '';
			validateFile().always(() => {
				/* selected image or new image */
				if (selectedImg) {
					var newImg = selectedImg;
				} else {
					var newImg = editor.document.createElement('img');
				}
				newImg.setAttribute('src', src);
				src = null;
				/* Set attributes */
				newImg.setAttribute('alt', self.getValueOf('tab-properties', 'alt').replace(/^\s+/, '').replace(/\s+$/, ''));
				let attr = {
						width: ['width', 'width:#;', 'integer', 1],
						height: ['height', 'height:#;', 'integer', 1],
						vmargin: ['vspace', 'margin-top:#;margin-bottom:#;', 'integer', 0],
						hmargin: ['hspace', 'margin-left:#;margin-right:#;', 'integer', 0],
						align: ['align', ''],
						border: ['border', 'border:# solid black;', 'integer', 0]
					},
					css = [],
					value,
					cssValue,
					attrValue,
					unit,
					k;
				for (k in attr) {
					value = self.getValueOf('tab-properties', k);
					attrValue = value;
					cssValue = value;
					unit = 'px';

					if (k == 'align') {
						switch (value) {
							case 'top':
							case 'bottom':
								attr[k][1] = 'vertical-align:#;';
								break;
							case 'left':
							case 'right':
								attr[k][1] = 'float:#;';
								break;
							default:
								value = null;
								break;
						}
					if (attr[k][2] == 'integer') {
						if (value.indexOf('%') >= 0) unit = '%';
						value = parseInt(value, 10);
						if (isNaN(value)) value = null;
						else if (value < attr[k][3]) value = null;
						if (value != null) {
							if (unit == '%') {
								attrValue = value + '%';
								cssValue = value + '%';
							} else {
								attrValue = value;
								cssValue = value + 'px';
							}
					if (value != null) {
						newImg.setAttribute(attr[k][0], attrValue);
						css.push(attr[k][1].replace(/#/g, cssValue));
					}
				if (css.length > 0) newImg.setAttribute('style', css.join(''));

				/* Insert new image */
				if (!selectedImg) editor.insertElement(newImg);
				/* Resize image */
				if (editor.plugins.imageresize) editor.plugins.imageresize.resize(editor, newImg, 800, 800);
				editor.updateElement();
			});
/* {[The file is published on the basis of YetiForce Public License 5.0 that can be found in the following directory: licenses/LicenseEN.txt or yetiforce.com]} */
'use strict';
	init: function (editor) {
		if (editor.addFeature) {
			editor.addFeature({
				allowedContent: 'img[alt,id,!src]{width,height};'
			});
		}
		editor.on('paste', (event, a, b) => {
			this.onPaste(event);
		});
		const pluginName = 'base64image-dialog';
		editor.ui.addToolbarGroup('base64image', 'insert');
		editor.ui.addButton('base64image', {
			label: editor.lang.common.image,
		editor.addCommand(
		editor.on('doubleclick', function (evt) {
				editor.getSelection().selectElement(evt.data.element);
		if (editor.addMenuItem) {
			editor.addMenuGroup('imageToBase64Group');
			editor.addMenuItem('imageToBase64Item', {
				label: editor.lang.common.image,
		if (editor.contextMenu) {
			editor.contextMenu.addListener(function (element) {
					editor.getSelection().selectElement(element);
	},
	onPaste: function (event) {
		const self = this,
			allowedTypes = 'image/jpeg|image/png|image/gif',
			dataTransfer = event.data.dataTransfer,
			editor = event.editor,
			count = dataTransfer.getFilesCount();
		for (let index = 0; index < count; index++) {
			let file = dataTransfer.getFile(index);
			if (file.type.match(allowedTypes)) {
				self
					.validateFile(file, editor)
					.done(function (base) {
						let image,
							selectedImg = editor.getSelection();
						if (selectedImg) selectedImg = selectedImg.getSelectedElement();
						if (!selectedImg || selectedImg.getName() !== 'img') selectedImg = null;
						if (selectedImg) {
							image = selectedImg;
						} else {
							image = editor.document.createElement('img');
						}
						image.setAttribute('src', base);
						if (!selectedImg) editor.insertElement(image);
					})
					.fail(function (error) {
						editor.showNotification(error, 'warning');
					});
			} else {
				editor.showNotification(
					app.vtranslate('JS_INVALID_FILE_TYPE') +
						'<br>' +
						app.vtranslate('JS_AVAILABLE_FILE_TYPES') +
						': ' +
						allowedTypes.replace(/\|/g, ', '),
					'warning'
				);
			}
		}
	},
	validateFile: function (file, editor) {
		const aDeferred = jQuery.Deferred();
		if (file.size > CONFIG['maxUploadLimit']) {
			aDeferred.reject(app.vtranslate('JS_UPLOADED_FILE_SIZE_EXCEEDS'));
		}
		this.readAndValidate(file, editor)
			.done(function (base) {
				aDeferred.resolve(base);
			})
			.fail(function (error) {
				aDeferred.reject(error);
			});
		return aDeferred.promise();
	},
	readAndValidate: function (file, editor) {
		const aDeferred = jQuery.Deferred(),
			fieldInfo = $(editor.element.$).data('fieldinfo');
		let length = editor.getData().length,
			selectedImg = editor.getSelection();
		if (selectedImg) selectedImg = selectedImg.getSelectedElement();
		if (!selectedImg || selectedImg.getName() !== 'img') selectedImg = null;
		if (selectedImg) {
			length = length - selectedImg.getOuterHtml().length;
		}
		const fileReader = new FileReader();
		fileReader.onload = function (evt) {
			length += evt.target.result.length;
			if (length > fieldInfo['maximumlength']) {
				return aDeferred.reject(app.vtranslate('JS_MAXIMUM_TEXT_SIZE_IN_BYTES') + ' ' + fieldInfo['maximumlength']);
			}
			AppConnector.request({
				module: app.getModuleName(),
				action: 'Fields',
				mode: 'validateFile',
				fieldName: fieldInfo['name'],
				base64: evt.target.result
			})
				.done((data) => {
					if (data.result.validate) {
						aDeferred.resolve(evt.target.result);
					} else {
						aDeferred.reject(data.result.validateError);
					}
				})
				.fail(function () {
					aDeferred.reject();
				});
		};
		fileReader.readAsDataURL(file);
		return aDeferred.promise();