Issues (3882)

Security Analysis    39 potential vulnerabilities

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  Response Splitting (9)
Response Splitting can be used to send arbitrary responses.
  File Manipulation (2)
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  File Exposure (7)
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Code Injection (13)
Code Injection enables an attacker to execute arbitrary code on the server.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Cross-Site Scripting (8)
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  Header Injection
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

app/Condition.php (3 issues)

1
<?php
2
3
/**
4
 * Condition main class.
5
 *
6
 * @package App
7
 *
8
 * @copyright YetiForce S.A.
9
 * @license   YetiForce Public License 6.5 (licenses/LicenseEN.txt or yetiforce.com)
10
 * @author    Tomasz Kur <[email protected]>
11
 * @author    Mariusz Krzaczkowski <[email protected]>
12
 * @author    RadosÅ‚aw Skrzypczak <[email protected]>
13
 */
14
15
namespace App;
16
17
/**
18
 * Condition main class.
19
 */
20
class Condition
21
{
22
	/**
23
	 * @var array Data filter list.
24
	 */
25
	const DATE_OPERATORS = [
26
		'custom' => ['label' => 'LBL_CUSTOM'],
27
		'smallerthannow' => ['label' => 'LBL_SMALLER_THAN_NOW'],
28
		'greaterthannow' => ['label' => 'LBL_GREATER_THAN_NOW'],
29
		'prevfy' => ['label' => 'LBL_PREVIOUS_FY'],
30
		'thisfy' => ['label' => 'LBL_CURRENT_FY'],
31
		'nextfy' => ['label' => 'LBL_NEXT_FY'],
32
		'prevfq' => ['label' => 'LBL_PREVIOUS_FQ'],
33
		'thisfq' => ['label' => 'LBL_CURRENT_FQ'],
34
		'nextfq' => ['label' => 'LBL_NEXT_FQ'],
35
		'previousworkingday' => ['label' => 'LBL_PREVIOUS_WORKING_DAY'],
36
		'nextworkingday' => ['label' => 'LBL_NEXT_WORKING_DAY'],
37
		'yesterday' => ['label' => 'LBL_YESTERDAY'],
38
		'today' => ['label' => 'LBL_TODAY'],
39
		'untiltoday' => ['label' => 'LBL_UNTIL_TODAY'],
40
		'tomorrow' => ['label' => 'LBL_TOMORROW'],
41
		'lastweek' => ['label' => 'LBL_LAST_WEEK'],
42
		'thisweek' => ['label' => 'LBL_CURRENT_WEEK'],
43
		'nextweek' => ['label' => 'LBL_NEXT_WEEK'],
44
		'lastmonth' => ['label' => 'LBL_LAST_MONTH'],
45
		'thismonth' => ['label' => 'LBL_CURRENT_MONTH'],
46
		'nextmonth' => ['label' => 'LBL_NEXT_MONTH'],
47
		'last7days' => ['label' => 'LBL_LAST_7_DAYS'],
48
		'last15days' => ['label' => 'LBL_LAST_15_DAYS'],
49
		'last30days' => ['label' => 'LBL_LAST_30_DAYS'],
50
		'last60days' => ['label' => 'LBL_LAST_60_DAYS'],
51
		'last90days' => ['label' => 'LBL_LAST_90_DAYS'],
52
		'last120days' => ['label' => 'LBL_LAST_120_DAYS'],
53
		'next15days' => ['label' => 'LBL_NEXT_15_DAYS'],
54
		'next30days' => ['label' => 'LBL_NEXT_30_DAYS'],
55
		'next60days' => ['label' => 'LBL_NEXT_60_DAYS'],
56
		'next90days' => ['label' => 'LBL_NEXT_90_DAYS'],
57
		'next120days' => ['label' => 'LBL_NEXT_120_DAYS'],
58
		'moreThanDaysAgo' => ['label' => 'LBL_DATE_CONDITION_MORE_THAN_DAYS_AGO'],
59
	];
60
61
	/**
62
	 * @var string[] List of field comparison operators
63
	 */
64
	const FIELD_COMPARISON_OPERATORS = ['ef', 'nf', 'lf', 'gf', 'mf', 'hf'];
65
66
	/**
67
	 * @var string[] Supported advanced filter operations.
68
	 */
69
	const STANDARD_OPERATORS = [
70
		'e' => 'LBL_EQUALS',
71
		'n' => 'LBL_NOT_EQUAL_TO',
72
		's' => 'LBL_STARTS_WITH',
73
		'ew' => 'LBL_ENDS_WITH',
74
		'c' => 'LBL_CONTAINS',
75
		'ch' => 'LBL_CONTAINS_HIERARCHY',
76
		'k' => 'LBL_DOES_NOT_CONTAIN',
77
		'kh' => 'LBL_DOES_NOT_CONTAIN_HIERARCHY',
78
		'l' => 'LBL_LESS_THAN',
79
		'g' => 'LBL_GREATER_THAN',
80
		'm' => 'LBL_LESS_THAN_OR_EQUAL',
81
		'h' => 'LBL_GREATER_OR_EQUAL',
82
		'b' => 'LBL_BEFORE',
83
		'a' => 'LBL_AFTER',
84
		'bw' => 'LBL_BETWEEN',
85
		'y' => 'LBL_IS_EMPTY',
86
		'ny' => 'LBL_IS_NOT_EMPTY',
87
		'om' => 'LBL_CURRENTLY_LOGGED_USER',
88
		'nom' => 'LBL_USER_CURRENTLY_NOT_LOGGED',
89
		'ogr' => 'LBL_CURRENTLY_LOGGED_USER_GROUP',
90
		'ogu' => 'LBL_USERS_GROUP_LOGGED_IN_USER',
91
		'wr' => 'LBL_IS_WATCHING_RECORD',
92
		'nwr' => 'LBL_IS_NOT_WATCHING_RECORD',
93
		'hs' => 'LBL_HAS_CHANGED',
94
		'hst' => 'LBL_HAS_CHANGED_TO',
95
		'ro' => 'LBL_IS_RECORD_OPEN',
96
		'rc' => 'LBL_IS_RECORD_CLOSED',
97
		'nco' => 'LBL_NOT_CREATED_BY_OWNER',
98
		'ef' => 'LBL_EQUALS_FIELD',
99
		'nf' => 'LBL_NOT_EQUAL_TO_FIELD',
100
		'lf' => 'LBL_LESS_THAN_FIELD',
101
		'gf' => 'LBL_GREATER_THAN_FIELD',
102
		'mf' => 'LBL_LESS_THAN_OR_EQUAL_FIELD',
103
		'hf' => 'LBL_GREATER_OR_EQUAL_FIELD',
104
	];
105
106
	/**
107
	 * @var string[] Operators without values.
108
	 */
109
	const OPERATORS_WITHOUT_VALUES = [
110
		'y', 'ny', 'om', 'nom', 'ogr', 'wr', 'nwr', 'hs', 'ro', 'rc', 'nco', 'ogu',
111
		'smallerthannow',
112
		'greaterthannow',
113
		'prevfy',
114
		'thisfy',
115
		'nextfy',
116
		'prevfq',
117
		'thisfq',
118
		'yesterday',
119
		'today',
120
		'untiltoday',
121
		'tomorrow',
122
		'lastweek',
123
		'thisweek',
124
		'nextweek',
125
		'lastmonth',
126
		'thismonth',
127
		'nextmonth',
128
		'last7days',
129
		'last15days',
130
		'last30days',
131
		'last60days',
132
		'last90days',
133
		'last120days',
134
		'next15days',
135
		'next30days',
136
		'next60days',
137
		'next90days',
138
		'next120days',
139
		'previousworkingday',
140
		'nextworkingday',
141
	];
142
143
	/**
144
	 * Vtiger_Record_Model instance cache.
145
	 *
146
	 * @var Vtiger_Record_Model[]
147
	 */
148
	private static $recordCache = [];
149
150
	/**
151
	 * Checks structure search_params.
152
	 *
153
	 * @param string $moduleName
154
	 * @param array  $searchParams
155
	 * @param bool   $convert
156
	 *
157
	 * @throws \App\Exceptions\IllegalValue
158
	 *
159
	 * @return array
160
	 */
161
	public static function validSearchParams(string $moduleName, array $searchParams, $convert = true): array
162
	{
163
		$searchParamsCount = \count($searchParams);
164
		if ($searchParamsCount > 2) {
165
			throw new Exceptions\IllegalValue("ERR_NUMBER_OF_ARGUMENTS_NOT_ALLOWED||{$searchParamsCount}|| > 2||" . Utils::varExport($searchParams, true), 406);
0 ignored issues
show
The call to App\Utils::varExport() has too many arguments starting with true. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

165
			throw new Exceptions\IllegalValue("ERR_NUMBER_OF_ARGUMENTS_NOT_ALLOWED||{$searchParamsCount}|| > 2||" . Utils::/** @scrutinizer ignore-call */ varExport($searchParams, true), 406);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
166
		}
167
		$fields = \Vtiger_Module_Model::getInstance($moduleName)->getFields();
168
		$result = [];
169
		foreach ($searchParams as $params) {
170
			$tempParam = [];
171
			foreach ($params as $param) {
172
				if (empty($param)) {
173
					continue;
174
				}
175
				$count = \count($param);
176
				if (3 !== $count && 4 !== $count) {
177
					throw new Exceptions\IllegalValue("ERR_NUMBER_OF_ARGUMENTS_NOT_ALLOWED||{$count}|| <> 3 or 4||" . Utils::varExport($param, true), 406);
178
				}
179
				[$relatedFieldName, $relatedModule, $referenceField] = array_pad(explode(':', $param[0]), 3, null);
180
				if ($relatedModule) {
181
					$relatedFields = \Vtiger_Module_Model::getInstance($relatedModule)->getFields();
182
					if (!isset($fields[$referenceField], $relatedFields[$relatedFieldName])) {
183
						throw new Exceptions\IllegalValue("ERR_FIELD_NOT_FOUND||{$param[0]}||" . Utils::varExport($param, true), 406);
184
					}
185
					$value = $relatedFields[$relatedFieldName]->getUITypeModel()->getDbConditionBuilderValue($param[2], $param[1]);
186
				} elseif (0 === strpos($param[0], 'relationColumn_') && preg_match('/(^relationColumn_)(\d+)$/', $param[0])) {
187
					$value = (int) $param[2];
188
				} else {
189
					if (!isset($fields[$param[0]])) {
190
						throw new Exceptions\IllegalValue("ERR_FIELD_NOT_FOUND||{$param[0]}||" . Utils::varExport($param, true), 406);
191
					}
192
					$value = $fields[$param[0]]->getUITypeModel()->getDbConditionBuilderValue($param[2], $param[1]);
193
				}
194
				if ($convert) {
195
					$param[2] = $value;
196
				}
197
				$tempParam[] = $param;
198
			}
199
			$result[] = $tempParam;
200
		}
201
		return $result;
202
	}
203
204
	/**
205
	 * Checks value search_value.
206
	 *
207
	 * @param string $value
208
	 * @param string $moduleName
209
	 * @param string $fieldName
210
	 * @param string $operator
211
	 *
212
	 * @return string
213
	 */
214
	public static function validSearchValue(string $value, string $moduleName, string $fieldName, string $operator): string
215
	{
216
		if ('' !== $value) {
217
			\Vtiger_Module_Model::getInstance($moduleName)->getField($fieldName)->getUITypeModel()->getDbConditionBuilderValue($value, $operator);
218
		}
219
		return $value;
220
	}
221
222
	/**
223
	 * Return condition from request.
224
	 *
225
	 * @param array $conditions
226
	 *
227
	 * @return array
228
	 */
229
	public static function getConditionsFromRequest(array $conditions): array
230
	{
231
		if (isset($conditions['rules'])) {
232
			foreach ($conditions['rules'] as &$condition) {
233
				if (isset($condition['condition'])) {
234
					$condition = static::getConditionsFromRequest($condition);
235
				} else {
236
					$operator = $condition['operator'];
237
					$value = $condition['value'] ?? '';
238
					if (!\in_array($operator, array_merge(self::OPERATORS_WITHOUT_VALUES, self::FIELD_COMPARISON_OPERATORS, array_keys(self::DATE_OPERATORS)))) {
239
						[$fieldName, $fieldModuleName,] = array_pad(explode(':', $condition['fieldname']), 3, false);
240
						$value = \Vtiger_Module_Model::getInstance($fieldModuleName)->getFieldByName($fieldName)
241
							->getUITypeModel()
242
							->getDbConditionBuilderValue($value, $operator);
243
					}
244
					$condition['value'] = $value;
245
				}
246
			}
247
		}
248
		return $conditions;
249
	}
250
251
	/**
252
	 * Check all conditions.
253
	 *
254
	 * @param array                $conditions
255
	 * @param \Vtiger_Record_Model $recordModel
256
	 *
257
	 * @return bool
258
	 */
259
	public static function checkConditions(array $conditions, \Vtiger_Record_Model $recordModel): bool
260
	{
261
		return self::parseConditions($conditions, $recordModel);
262
	}
263
264
	/**
265
	 * Parse conditions.
266
	 *
267
	 * @param array|null           $conditions
268
	 * @param \Vtiger_Record_Model $recordModel
269
	 *
270
	 * @return bool
271
	 */
272
	private static function parseConditions(?array $conditions, \Vtiger_Record_Model $recordModel): bool
273
	{
274
		if (empty($conditions) || empty($conditions['rules'])) {
275
			return true;
276
		}
277
		$result = false;
278
		$andCondition = 'AND' === $conditions['condition'];
279
		foreach ($conditions['rules'] as $rule) {
280
			if (isset($rule['condition'])) {
281
				$ruleResult = self::parseConditions($rule, $recordModel);
282
			} else {
283
				$ruleResult = self::checkCondition($rule, $recordModel);
284
			}
285
			if (!$andCondition && $ruleResult) {
286
				$result = true;
287
				break;
288
			}
289
			if ($andCondition && !$ruleResult) {
290
				$result = false;
291
				break;
292
			}
293
			$result = $ruleResult;
294
		}
295
		return $result;
296
	}
297
298
	/**
299
	 * Check one condition.
300
	 *
301
	 * @param array                $rule
302
	 * @param \Vtiger_Record_Model $recordModel
303
	 *
304
	 * @return bool
305
	 */
306
	public static function checkCondition(array $rule, \Vtiger_Record_Model $recordModel): bool
307
	{
308
		[$fieldName, $moduleName, $sourceFieldName] = array_pad(explode(':', $rule['fieldname']), 3, false);
309
		if ($sourceFieldName) {
310
			if ($recordModel->isEmpty($sourceFieldName)) {
311
				return false;
312
			}
313
			$sourceRecordModel = self::$recordCache[$recordModel->get($sourceFieldName)] ?? \Vtiger_Record_Model::getInstanceById($recordModel->get($sourceFieldName));
314
			$fieldModel = $sourceRecordModel->getField($fieldName);
315
		} elseif ($recordModel->getModuleName() === $moduleName) {
316
			$fieldModel = $recordModel->getField($fieldName);
317
		}
318
		if (empty($fieldModel)) {
319
			Log::error("Not found field model | Field name: $fieldName | Module: $moduleName", 'Condition');
320
			throw new \App\Exceptions\AppException('ERR_NOT_FOUND_FIELD_MODEL|' . $fieldName);
321
		}
322
		$className = '\App\Conditions\RecordFields\\' . ucfirst($fieldModel->getFieldDataType()) . 'Field';
323
		if (!class_exists($className)) {
324
			Log::error("Not found record field condition | Field name: $fieldName | Module: $moduleName | FieldDataType: " . ucfirst($fieldModel->getFieldDataType()), 'Condition');
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $fieldModel does not seem to be defined for all execution paths leading up to this point.
Loading history...
325
			throw new \App\Exceptions\AppException("ERR_NOT_FOUND_QUERY_FIELD_CONDITION|$fieldName|$className");
326
		}
327
		if (!empty($sourceFieldName)) {
328
			$recordField = new $className($sourceRecordModel, $fieldModel, $rule);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $sourceRecordModel does not seem to be defined for all execution paths leading up to this point.
Loading history...
329
			$recordField->setSource($recordModel, $sourceFieldName);
330
		} else {
331
			$recordField = new $className($recordModel, $fieldModel, $rule);
332
		}
333
		return $recordField->check();
334
	}
335
336
	/**
337
	 * Get field names from conditions.
338
	 *
339
	 * @param array $conditions
340
	 *
341
	 * @return array ['baseModule' => [], 'referenceModule' => []]
342
	 */
343
	public static function getFieldsFromConditions(array $conditions): array
344
	{
345
		$fields = ['baseModule' => [], 'referenceModule' => []];
346
		if (isset($conditions['rules'])) {
347
			foreach ($conditions['rules'] as &$condition) {
348
				if (isset($condition['condition'])) {
349
					$fields = array_merge_recursive($fields, static::getFieldsFromConditions($condition));
350
				} else {
351
					[$fieldName, $moduleName, $sourceFieldName] = array_pad(explode(':', $condition['fieldname']), 3, false);
352
					if ($sourceFieldName) {
353
						$fields['referenceModule'][$moduleName][$sourceFieldName] = $fieldName;
354
					} else {
355
						$fields['baseModule'][] = $fieldName;
356
					}
357
				}
358
			}
359
		}
360
		return $fields;
361
	}
362
363
	/**
364
	 * Remove field from conditions.
365
	 *
366
	 * @param string $baseModuleName The base name of the module for which conditions have been set
367
	 * @param array  $conditions
368
	 * @param string $moduleName     The module name of the field to be deleted.
369
	 * @param string $fieldName      The name of the field to be deleted
370
	 *
371
	 * @return array
372
	 */
373
	public static function removeFieldFromCondition(string $baseModuleName, array $conditions, string $moduleName, string $fieldName): array
374
	{
375
		if (isset($conditions['rules'])) {
376
			foreach ($conditions['rules'] as $key => &$condition) {
377
				if (isset($condition['condition'])) {
378
					$condition = static::removeFieldFromCondition($baseModuleName, $condition, $moduleName, $fieldName);
379
				} else {
380
					[$cFieldName, $cModuleName, $sourceFieldName] = array_pad(explode(':', $condition['fieldname']), 3, false);
381
					if (($fieldName === $cFieldName && $moduleName === $cModuleName) || ($sourceFieldName && $sourceFieldName === $fieldName && $moduleName === $baseModuleName)) {
382
						$condition = [];
383
					}
384
				}
385
				if (empty($condition)) {
386
					unset($conditions['rules'][$key]);
387
				}
388
			}
389
			if (empty($conditions['rules'] = array_filter($conditions['rules']))) {
390
				$conditions = [];
391
			}
392
		}
393
		return $conditions;
394
	}
395
396
	/**
397
	 * Checks structure advancedConditions.
398
	 *
399
	 * @param array $advancedConditions
400
	 *
401
	 * @return array
402
	 */
403
	public static function validAdvancedConditions(array $advancedConditions): array
404
	{
405
		if (!empty($advancedConditions['relationConditions']) && 0 != $advancedConditions['relationId']) {
406
			$advancedConditions['relationConditions'] = self::getConditionsFromRequest($advancedConditions['relationConditions']);
407
		}
408
		if (!empty($advancedConditions['relationColumns'])) {
409
			array_map(function ($v) {
410
				if (!\App\Validator::integer($v)) {
411
					throw new \App\Exceptions\IllegalValue('ERR_NOT_ALLOWED_VALUE||' . $v, 406);
412
				}
413
			}, $advancedConditions['relationColumns']);
414
		}
415
		return $advancedConditions;
416
	}
417
}
418