PluginProbe
ManageWP Worker / 4.9.37
ManageWP Worker v4.9.37
4.9.38 4.9.37 4.9.36 4.9.35 4.9.34 3.8.7 3.8.8 3.9.0 3.9.1 3.9.10 3.9.11 3.9.12 3.9.13 3.9.14 3.9.15 3.9.16 3.9.17 3.9.18 3.9.19 3.9.2 3.9.20 3.9.21 3.9.22 3.9.23 3.9.24 All 73 releases
worker / src / MWP / Action / DownloadFile.php

DownloadFile.php in ManageWP Worker 4.9.37, at src/MWP/Action/DownloadFile.php

141 lines 5.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*
3 * This file is part of the ManageWP Worker plugin.
4 *
5 * (c) ManageWP LLC <[email protected]>
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10
11 class MWP_Action_DownloadFile extends MWP_Action_Abstract
12 {
13 const DOWNLOAD_FAILED = 12;
14
15 public function execute(array $params)
16 {
17 $requestedFiles = $params['files'];
18
19 // Validate that every requested path sits within the WordPress installation
20 // root (ABSPATH). This prevents path traversal attacks where a crafted path
21 // like ../../etc/passwd could escape the intended directory boundary.
22 //
23 // We deliberately avoid realpath() for the boundary check because it follows
24 // symlinks, which would block legitimate sites that symlink directories outside
25 // ABSPATH (e.g. an uploads folder pointing to network storage). Instead we
26 // collapse . and .. via string operations only, preserving intentional symlinks.
27 // realpath() is still called afterwards, but only to verify the file exists —
28 // its resolved value is not used for the security comparison.
29 //
30 // DIRECTORY_SEPARATOR is appended to $allowedBase so that a sibling path like
31 // /var/www/html-other cannot pass a prefix check intended for /var/www/html.
32 $allowedBase = rtrim(ABSPATH, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
33
34 // Collect normalised paths so that all file operations below use the
35 // same values that were security-checked. Using the raw input after
36 // validation (validate-then-use-different-value) would be unsafe.
37 $normalisedFiles = array();
38 foreach ($requestedFiles as $filePath) {
39 // Make relative paths absolute so the boundary check works correctly.
40 if (!path_is_absolute($filePath)) {
41 $filePath = ABSPATH . $filePath;
42 }
43
44 // Collapse . and .. segments without following symlinks.
45 $parts = explode('/', str_replace('\\', '/', $filePath));
46 $normalised = array();
47 foreach ($parts as $part) {
48 if ($part === '..') {
49 array_pop($normalised);
50 } elseif ($part !== '' && $part !== '.') {
51 $normalised[] = $part;
52 }
53 }
54 $normalisedPath = DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $normalised);
55
56 // Boundary check against the .. -clean path (symlinks left intact).
57 if (strpos($normalisedPath . DIRECTORY_SEPARATOR, $allowedBase) !== 0) {
58 return array('message' => self::DOWNLOAD_FAILED);
59 }
60
61 // Verify the file actually exists on disk.
62 if (realpath($filePath) === false) {
63 return array('message' => self::DOWNLOAD_FAILED);
64 }
65
66 $normalisedFiles[] = $normalisedPath;
67 }
68
69 if (count($normalisedFiles) > 1 || is_dir($normalisedFiles[0])) {
70 $requestedFile = $this->archiveFiles($requestedFiles);
71 } else {
72 $requestedFile = $requestedFiles[0];
73 }
74
75 $fp = fopen($requestedFile, "r");
76 if (!$fp) {
77 return array('message' => self::DOWNLOAD_FAILED);
78 }
79
80 $result = new MWP_FileManager_Model_DownloadFilesResult();
81 $file = new MWP_FileManager_Model_Files();
82 $file->setPathname($requestedFile);
83 $file->setStream(MWP_Stream_Stream::factory($fp));
84 $result->addFile($file);
85
86 return $result;
87 }
88
89 private function archiveFiles($files)
90 {
91 $filePath = WP_CONTENT_DIR."/mwp-download/";
92 if (!file($filePath)) {
93 mkdir($filePath);
94 $indexPHP = fopen($filePath."index.php", 'w+');
95 fwrite($indexPHP, "<?php \n\n // Silence is golden. \n");
96 fclose($indexPHP);
97 }
98
99 $randomString = mwp_generate_uuid4();
100
101 $zipName = $filePath.$randomString.".zip";
102 if (!class_exists('ZipArchive')) {
103 $escapedFiles = array();
104 foreach ($files as $file) {
105 $escapedFiles[] = escapeshellarg($file);
106 }
107
108 exec('zip -r ' . $zipName . ' ' . join(' ', $escapedFiles), $output, $exitCode);
109 return $zipName;
110 }
111
112 /** @handled class */
113 $zip = new ZipArchive();
114
115 /** @handled static */
116 $zip->open($zipName, ZipArchive::CREATE);
117
118 foreach ($files as $filePath) {
119 if (!is_dir($filePath)) {
120 $zip->addFile($filePath);
121 continue;
122 }
123
124 $filesFromDir = $this->getFilesRecursive($filePath);
125 foreach ($filesFromDir as $file) {
126 if (is_dir($file)) {
127 continue;
128 }
129 $zip->addFile($file->getRealPath(), $file->getPath()."/".$file->getFilename());
130 }
131 }
132 $zip->close();
133 return $zipName;
134 }
135
136 private function getFilesRecursive($path)
137 {
138 return new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path), RecursiveIteratorIterator::LEAVES_ONLY);
139 }
140 }
141