Zip::getLocalPath()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
ccs 0
cts 0
cp 0
rs 10
cc 1
nc 1
nop 1
crap 2
1
<?php
2
/**
3
 * A file archive, compressed with Zip.
4
 *
5
 * @package App
6
 *
7
 * @copyright YetiForce S.A.
8
 * @license   YetiForce Public License 6.5 (licenses/LicenseEN.txt or yetiforce.com)
9
 * @author    Mariusz Krzaczkowski <[email protected]>
10
 * @author    Radosław Skrzypczak <[email protected]>
11
 */
12
13
namespace App;
14
15
/**
16
 * Zip class.
17
 */
18
class Zip extends \ZipArchive
19
{
20
	/**
21
	 * Files extension for extract.
22
	 *
23
	 * @var array
24
	 */
25
	protected $onlyExtensions;
26
27
	/**
28
	 * Illegal extensions for extract.
29
	 *
30
	 * @var array
31
	 */
32
	protected $illegalExtensions;
33
34
	/**
35
	 * Check files before unpacking.
36
	 *
37
	 * @var bool
38
	 */
39
	protected $checkFiles = true;
40
41
	/**
42
	 * Open file and initialization unpack.
43
	 *
44
	 * @param bool  $fileName
45
	 * @param array $options
46
	 *
47
	 * @throws Exceptions\AppException
48 13
	 *
49
	 * @return bool|Zip
50 13
	 */
51 1
	public static function openFile($fileName = false, $options = [])
52
	{
53 12
		if (!$fileName) {
54 12
			throw new \App\Exceptions\AppException('No file name');
55 1
		}
56
		$zip = new self($fileName, $options);
0 ignored issues
show
Unused Code introduced by
The call to App\Zip::__construct() has too many arguments starting with $fileName. ( Ignorable by Annotation )

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

56
		$zip = /** @scrutinizer ignore-call */ new self($fileName, $options);

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...
57 11
		if (!file_exists($fileName) || !$zip->open($fileName)) {
0 ignored issues
show
Bug introduced by
$fileName of type true is incompatible with the type string expected by parameter $filename of file_exists(). ( Ignorable by Annotation )

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

57
		if (!file_exists(/** @scrutinizer ignore-type */ $fileName) || !$zip->open($fileName)) {
Loading history...
Bug introduced by
$fileName of type true is incompatible with the type string expected by parameter $filename of ZipArchive::open(). ( Ignorable by Annotation )

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

57
		if (!file_exists($fileName) || !$zip->open(/** @scrutinizer ignore-type */ $fileName)) {
Loading history...
58
			throw new \App\Exceptions\AppException('Unable to open the zip file');
59
		}
60 11
		if (!$zip->checkFreeSpace()) {
61 8
			throw new \App\Exceptions\AppException('The content of the zip file is too large');
62
		}
63 11
		foreach ($options as $key => $value) {
64
			$zip->{$key} = $value;
65
		}
66
		return $zip;
67
	}
68
69
	/**
70
	 * Open file for create zip file.
71
	 *
72
	 * @param string $fileName
73
	 *
74
	 * @throws \App\Exceptions\AppException
75 3
	 *
76
	 * @return \App\Zip
77 3
	 */
78 3
	public static function createFile($fileName)
79
	{
80
		$zip = new self();
81 3
		if (true !== $zip->open($fileName, self::CREATE | self::OVERWRITE)) {
82
			throw new \App\Exceptions\AppException('Unable to create the zip file');
83
		}
84
		return $zip;
85
	}
86
87
	/**
88
	 * Function to extract files.
89
	 *
90
	 * @param      $toDir Target directory
0 ignored issues
show
Bug introduced by
The type App\Target was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
91
	 * @param bool $close
92 7
	 *
93
	 * @return string|string[] Unpacked files
94 7
	 */
95 1
	public function unzip($toDir, bool $close = true)
96
	{
97 7
		if (\is_string($toDir)) {
98 7
			$toDir = [$toDir];
99 7
		}
100 7
		$files = $created = [];
101 7
		foreach ($toDir as $dir => $target) {
102 7
			for ($i = 0; $i < $this->numFiles; ++$i) {
103 4
				$zipPath = $this->getNameIndex($i);
104
				$path = \str_replace('\\', '/', $zipPath);
105 7
				if ((!\is_numeric($dir) && 0 !== strpos($path, $dir . '/')) || $this->validateFile($zipPath)) {
106 7
					continue;
107 7
				}
108 7
				$files[] = $zipPath;
109 7
				$file = $target . '/' . (\is_numeric($dir) ? $path : substr($path, \strlen($dir) + 1));
110 7
				$fileDir = \dirname($file);
111
				if (!isset($created[$fileDir])) {
112 7
					if (!is_dir($fileDir)) {
113
						mkdir($fileDir, 0755, true);
114 7
					}
115
					$created[$fileDir] = true;
116 7
				}
117 7
				if (!$this->isDir($path)) {
118 7
					// Read from Zip and write to disk
119 7
					$fpr = $this->getStream($zipPath);
120
					$fpw = fopen($file, 'w');
121 7
					while ($data = fread($fpr, 1024)) {
122 7
						fwrite($fpw, $data);
123
					}
124
					fclose($fpr);
125
					fclose($fpw);
126 7
				}
127 7
			}
128
		}
129 7
		if ($close) {
130
			$this->close();
131
		}
132
		return $files;
133
	}
134
135
	/**
136
	 * Simple extract the archive contents.
137
	 *
138
	 * @param string $toDir
139
	 *
140
	 * @throws \App\Exceptions\AppException
141 1
	 *
142
	 * @return array
143 1
	 */
144
	public function extract(string $toDir)
145
	{
146 1
		if (!is_dir($toDir) && !mkdir($toDir, 0755, true) && !is_dir($toDir)) {
147 1
			throw new \App\Exceptions\AppException('Directory unable to create it');
148 1
		}
149 1
		$fileList = [];
150 1
		for ($i = 0; $i < $this->numFiles; ++$i) {
151
			$path = $this->getNameIndex($i);
152 1
			if ($this->validateFile(\str_replace('\\', '/', $path))) {
153
				continue;
154 1
			}
155 1
			$fileList[] = $path;
156
		}
157
		$this->extractTo($toDir, $fileList);
158
		return $fileList;
159
	}
160
161
	/**
162
	 * Check illegal characters.
163
	 *
164
	 * @param string $path
165 8
	 *
166
	 * @return bool
167 8
	 */
168
	public function validateFile(string $path)
169
	{
170 8
		if (!Validator::path($path)) {
171 8
			return true;
172 3
		}
173 3
		$validate = false;
174
		if ($this->checkFiles && !$this->isDir($path)) {
175
			$extension = pathinfo($path, PATHINFO_EXTENSION);
176 3
			if (isset($this->onlyExtensions) && !\in_array($extension, $this->onlyExtensions)) {
177
				$validate = true;
178
			}
179 3
			if (isset($this->illegalExtensions) && \in_array($extension, $this->illegalExtensions)) {
180 3
				$validate = true;
181 3
			}
182 3
			$stat = $this->statName($path);
183 3
			$fileInstance = \App\Fields\File::loadFromInfo([
184 3
				'content' => $this->getFromName($path),
185
				'path' => $this->getLocalPath($path),
186
				'name' => basename($path),
187 3
				'size' => $stat['size'],
188 2
				'validateAllCodeInjection' => true,
189
			]);
190
			if (!$fileInstance->validate()) {
191 8
				$validate = true;
192
			}
193
		}
194
		return $validate;
195
	}
196
197
	/**
198
	 * Check if the file path is directory.
199
	 *
200
	 * @param string $filePath
201 8
	 *
202
	 * @return bool
203 8
	 */
204 6
	public function isDir($filePath)
205
	{
206 8
		if ('/' === substr($filePath, -1, 1)) {
207
			return true;
208
		}
209
		return false;
210
	}
211
212
	/**
213
	 * Function to extract single file.
214
	 *
215
	 * @param string $compressedFileName
216
	 * @param string $targetFileName
217
	 *
218
	 * @return bool
219
	 */
220
	public function unzipFile($compressedFileName, $targetFileName)
221
	{
222
		return copy($this->getLocalPath($compressedFileName), $targetFileName);
223
	}
224
225
	/**
226
	 * Get compressed file path.
227
	 *
228
	 * @param string $compressedFileName
229 3
	 *
230
	 * @return string
231 3
	 */
232
	public function getLocalPath($compressedFileName)
233
	{
234
		return "zip://{$this->filename}#{$compressedFileName}";
235
	}
236
237
	/**
238
	 * Check free disk space.
239 11
	 *
240
	 * @return bool
241 11
	 */
242 11
	public function checkFreeSpace()
243 11
	{
244 11
		$df = disk_free_space(ROOT_DIRECTORY . \DIRECTORY_SEPARATOR);
245 11
		$size = 0;
246
		for ($i = 0; $i < $this->numFiles; ++$i) {
247 11
			$stat = $this->statIndex($i);
248
			$size += $stat['size'];
249
		}
250
		return $df > $size;
251
	}
252
253
	/**
254
	 * Copy the directory on the disk into zip file.
255
	 *
256
	 * @param string $dir
257 2
	 * @param string $localName
258
	 * @param bool   $relativePath
259 2
	 */
260
	public function addDirectory(string $dir, string $localName = '', bool $relativePath = false)
261
	{
262 2
		if ($localName) {
263 2
			$localName .= '/';
264 2
		}
265 2
		$path = realpath($dir);
266 2
		$files = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path), \RecursiveIteratorIterator::LEAVES_ONLY);
267 2
		$pathToTrim = $relativePath ? $path : ROOT_DIRECTORY;
268 2
		foreach ($files as $file) {
269
			if (!$file->isDir()) {
270
				$filePath = $file->getRealPath();
271 2
				$zipPath = str_replace(\DIRECTORY_SEPARATOR, '/', Fields\File::getLocalPath($filePath, $pathToTrim));
272
				$this->addFile($filePath, $localName . $zipPath);
273
			}
274
		}
275
	}
276
277
	/**
278
	 * Push out the file content for download.
279
	 *
280
	 * @param string $name
281
	 */
282
	public function download(string $name)
283
	{
284
		$fileName = $this->filename;
285
		$this->close();
286
		header('cache-control: private, max-age=120, must-revalidate');
287
		header('pragma: no-cache');
288
		header('expires: 0');
289
		header('content-type: application/zip');
290
		header('content-disposition: attachment; filename="' . $name . '.zip";');
0 ignored issues
show
Security Response Splitting introduced by
'content-disposition: at...e="' . $name . '.zip";' can contain request data and is used in response header context(s) leading to a potential security vulnerability.

3 paths for user data to reach this point

  1. Path: Read from $_REQUEST, and Request::__construct() is called in api/webservice/Core/Request.php on line 56
  1. Read from $_REQUEST, and Request::__construct() is called
    in api/webservice/Core/Request.php on line 56
  2. Enters via parameter $rawValues
    in app/Request.php on line 110
  3. $rawValues is assigned to property Request::$rawValues
    in app/Request.php on line 112
  4. Read from property Request::$rawValues, and Data is passed through purifyByType(), and $this->purifiedValuesByType[$key][$type] = App\Purifier::purifyByType($this->rawValues[$key], $type, $convert) is returned
    in app/Request.php on line 170
  5. Time::formatToDB() is called
    in modules/Settings/BusinessHours/actions/Save.php on line 29
  6. Enters via parameter $time
    in app/Fields/Time.php on line 41
  7. DateTimeField::__construct() is called
    in app/Fields/Time.php on line 44
  8. Enters via parameter $value
    in include/fields/DateTimeField.php on line 31
  9. $value is assigned to property DateTimeField::$datetime
    in include/fields/DateTimeField.php on line 38
  10. Read from property DateTimeField::$datetime, and Data is passed through explode(), and explode(' ', $this->datetime, 2) is assigned to $value
    in include/fields/DateTimeField.php on line 47
  11. Data is passed through convertToDBFormat(), and self::convertToDBFormat($value[0]) is assigned to $insert_date
    in include/fields/DateTimeField.php on line 53
  12. $insert_date is returned
    in include/fields/DateTimeField.php on line 55
  13. new DateTimeField($value)->getDBInsertDateValue() is returned
    in app/Fields/Date.php on line 168
  14. App\Validator::dateInUserFormat($input) ? $convert ? App\Fields\Date::formatToDB($input) : $input : null is assigned to $value
    in app/Purifier.php on line 451
  15. $value is returned
    in app/Purifier.php on line 569
  16. App\Purifier::purifyByType($this->rawValues[$key], $type, $convert) is assigned to property Request::$purifiedValuesByType
    in app/Request.php on line 170
  17. Read from property Request::$purifiedValuesByType, and $this->purifiedValuesByType[$key][$type] is returned
    in app/Request.php on line 167
  18. $request->getByType('lang', 1) is assigned to $lang
    in modules/Settings/LangManagement/actions/Export.php on line 21
  19. LanguageExport::exportLanguage() is called
    in modules/Settings/LangManagement/actions/Export.php on line 24
  20. Enters via parameter $languageCode
    in vtlib/Vtiger/LanguageExport.php on line 44
  21. Zip::download() is called
    in vtlib/Vtiger/LanguageExport.php on line 67
  22. Enters via parameter $name
    in app/Zip.php on line 282
  2. Path: Read from $_REQUEST, and Request::__construct() is called in app/Request.php on line 728
  1. Read from $_REQUEST, and Request::__construct() is called
    in app/Request.php on line 728
  2. Enters via parameter $rawValues
    in app/Request.php on line 110
  3. $rawValues is assigned to property Request::$rawValues
    in app/Request.php on line 112
  4. Read from property Request::$rawValues, and Data is passed through purifyByType(), and $this->purifiedValuesByType[$key][$type] = App\Purifier::purifyByType($this->rawValues[$key], $type, $convert) is returned
    in app/Request.php on line 170
  5. Time::formatToDB() is called
    in modules/Settings/BusinessHours/actions/Save.php on line 29
  6. Enters via parameter $time
    in app/Fields/Time.php on line 41
  7. DateTimeField::__construct() is called
    in app/Fields/Time.php on line 44
  8. Enters via parameter $value
    in include/fields/DateTimeField.php on line 31
  9. $value is assigned to property DateTimeField::$datetime
    in include/fields/DateTimeField.php on line 38
  10. Read from property DateTimeField::$datetime, and Data is passed through explode(), and explode(' ', $this->datetime, 2) is assigned to $value
    in include/fields/DateTimeField.php on line 47
  11. Data is passed through convertToDBFormat(), and self::convertToDBFormat($value[0]) is assigned to $insert_date
    in include/fields/DateTimeField.php on line 53
  12. $insert_date is returned
    in include/fields/DateTimeField.php on line 55
  13. new DateTimeField($value)->getDBInsertDateValue() is returned
    in app/Fields/Date.php on line 168
  14. App\Validator::dateInUserFormat($input) ? $convert ? App\Fields\Date::formatToDB($input) : $input : null is assigned to $value
    in app/Purifier.php on line 451
  15. $value is returned
    in app/Purifier.php on line 569
  16. App\Purifier::purifyByType($this->rawValues[$key], $type, $convert) is assigned to property Request::$purifiedValuesByType
    in app/Request.php on line 170
  17. Read from property Request::$purifiedValuesByType, and $this->purifiedValuesByType[$key][$type] is returned
    in app/Request.php on line 167
  18. $request->getByType('lang', 1) is assigned to $lang
    in modules/Settings/LangManagement/actions/Export.php on line 21
  19. LanguageExport::exportLanguage() is called
    in modules/Settings/LangManagement/actions/Export.php on line 24
  20. Enters via parameter $languageCode
    in vtlib/Vtiger/LanguageExport.php on line 44
  21. Zip::download() is called
    in vtlib/Vtiger/LanguageExport.php on line 67
  22. Enters via parameter $name
    in app/Zip.php on line 282
  3. Path: DateTimeField::__construct() is called in app/Fields/Time.php on line 44
  1. DateTimeField::__construct() is called
    in app/Fields/Time.php on line 44
  2. Enters via parameter $value
    in include/fields/DateTimeField.php on line 31
  3. $value is assigned to property DateTimeField::$datetime
    in include/fields/DateTimeField.php on line 38
  4. Read from property DateTimeField::$datetime, and Data is passed through explode(), and explode(' ', $this->datetime) is assigned to $date_value
    in include/fields/DateTimeField.php on line 299
  5. Data is passed through convertToUserFormat(), and self::convertToUserFormat($date_value) is returned
    in include/fields/DateTimeField.php on line 308
  6. new DateTimeField($value)->getDisplayDate() is returned
    in app/Fields/Date.php on line 113
  7. $convertTimeZone ? App\Fields\Date::formatToDisplay(date('Y-m-d'), false) : date('Y-m-d') is assigned to $date
    in app/Fields/Time.php on line 43
  8. DateTimeField::__construct() is called
    in app/Fields/Time.php on line 44
  9. Enters via parameter $value
    in include/fields/DateTimeField.php on line 31
  10. $value is assigned to property DateTimeField::$datetime
    in include/fields/DateTimeField.php on line 38
  11. Read from property DateTimeField::$datetime, and Data is passed through explode(), and explode(' ', $this->datetime, 2) is assigned to $value
    in include/fields/DateTimeField.php on line 47
  12. Data is passed through convertToDBFormat(), and self::convertToDBFormat($value[0]) is assigned to $insert_date
    in include/fields/DateTimeField.php on line 53
  13. $insert_date is returned
    in include/fields/DateTimeField.php on line 55
  14. new DateTimeField($value)->getDBInsertDateValue() is returned
    in app/Fields/Date.php on line 168
  15. App\Validator::dateInUserFormat($input) ? $convert ? App\Fields\Date::formatToDB($input) : $input : null is assigned to $value
    in app/Purifier.php on line 451
  16. $value is returned
    in app/Purifier.php on line 569
  17. App\Purifier::purifyByType($this->rawValues[$key], $type, $convert) is assigned to property Request::$purifiedValuesByType
    in app/Request.php on line 170
  18. Read from property Request::$purifiedValuesByType, and $this->purifiedValuesByType[$key][$type] is returned
    in app/Request.php on line 167
  19. $request->getByType('lang', 1) is assigned to $lang
    in modules/Settings/LangManagement/actions/Export.php on line 21
  20. LanguageExport::exportLanguage() is called
    in modules/Settings/LangManagement/actions/Export.php on line 24
  21. Enters via parameter $languageCode
    in vtlib/Vtiger/LanguageExport.php on line 44
  22. Zip::download() is called
    in vtlib/Vtiger/LanguageExport.php on line 67
  23. Enters via parameter $name
    in app/Zip.php on line 282

Response Splitting Attacks

Allowing an attacker to set a response header, opens your application to response splitting attacks; effectively allowing an attacker to send any response, he would like.

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
291
		header('accept-ranges: bytes');
292
		header('content-length: ' . filesize($fileName));
293
		readfile($fileName);
294
		unlink($fileName);
295
	}
296
}
297