PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.2.0
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.2.0
2.2.0 2.1.0 2.0.15 2.0.12 2.0.10 2.0.4 2.0.1 2.0.0 1.95.3 1.95.2 1.95 1.91.6 trunk 1.11 1.12 1.13 1.20 1.21 1.22 1.23 1.30 1.31 1.32 1.35 1.40 All 43 releases
← All changes | app/Services/Libs/FileSystem.php +117 -2 2.1.0 → 2.2.0 View file →
@@ -2,8 +2,10 @@
2 2
3 3 namespace FluentBoards\App\Services\Libs;
4 4
5 5 use FluentBoards\Framework\Support\Arr;
6 +use FluentBoards\App\Services\Constant;
7 +use Exception;
6 8
7 9 class FileSystem
8 10 {
9 11 protected $subDir = '';
@@ -54,8 +56,117 @@
54 56 return $uploadDir['basedir'] . DIRECTORY_SEPARATOR . $fbsUploadDir;
55 57 }
56 58
57 59 /**
60 + * Prepare local storage with Apache rules or verified host-managed protection.
61 + * Installing rules alone does not prove the HTTP server enforces them.
62 + * Pass false to defer any required HTTP verification to cron.
63 + */
64 + public function _installUploadProtectionRules($allowProbe = true)
65 + {
66 + // Protect the root even when called on a board-specific filesystem instance.
67 + $directory = (new static())->_getDir();
68 + if (!wp_mkdir_p($directory)) {
69 + return false;
70 + }
71 +
72 + $path = $directory . '/.htaccess';
73 + $rules = [
74 + '<IfModule mod_authz_core.c>',
75 + 'Require all denied',
76 + '</IfModule>',
77 + '<IfModule !mod_authz_core.c>',
78 + 'Order deny,allow',
79 + 'Deny from all',
80 + '</IfModule>',
81 + ];
82 +
83 + // Hosts may lock security configuration while leaving upload storage writable.
84 + if (is_file($path) && is_readable($path)) {
85 + $contents = str_replace("\r\n", "\n", file_get_contents($path));
86 + if (preg_match_all('/^# BEGIN Fluent Boards\n(.*?)^# END Fluent Boards$/ms', $contents, $blocks) === 1) {
87 + $existingRules = array_values(array_filter(array_map('trim', explode("\n", $blocks[1][0])), function ($line) {
88 + return $line !== '' && strpos($line, '#') !== 0;
89 + }));
90 + if ($existingRules === $rules) {
91 + return true;
92 + }
93 + }
94 + }
95 +
96 + if (!function_exists('insert_with_markers')) {
97 + require_once ABSPATH . 'wp-admin/includes/misc.php';
98 + }
99 +
100 + if (insert_with_markers($path, 'Fluent Boards', $rules)) {
101 + return true;
102 + }
103 +
104 + // Unmarked, locked host rules and server-level restrictions need no rewrite.
105 + return $this->_checkUploadProtection($allowProbe) === true;
106 + }
107 +
108 + /**
109 + * Probe direct access without login cookies using a harmless temporary file.
110 + * Return true for denied access, false for exposure, or null when unverified.
111 + * Pass false to read only the cache and schedule a missing check in cron.
112 + */
113 + public function _checkUploadProtection($allowProbe = true)
114 + {
115 + $directory = (new static())->_getDir();
116 + $uploads = wp_upload_dir();
117 + $folder = apply_filters('fluent_boards/upload_folder_name', FLUENT_BOARDS_UPLOAD_DIR);
118 + $url = trailingslashit($uploads['baseurl']) . trim($folder, '/');
119 + $rulesPath = $directory . '/.htaccess';
120 + $rulesHash = is_file($rulesPath) && is_readable($rulesPath) ? hash_file('sha256', $rulesPath) : '';
121 + $cacheKey = Constant::UPLOAD_PROTECTION_CHECK_PREFIX . md5($directory . '|' . $url);
122 + $cached = get_transient($cacheKey);
123 + if (is_array($cached) && isset($cached['rules_hash']) && $cached['rules_hash'] === $rulesHash && array_key_exists('blocked', $cached)) {
124 + return $cached['blocked'];
125 + }
126 +
127 + if (!$allowProbe) {
128 + $hook = 'fluent_boards/check_upload_protection';
129 + if (!wp_next_scheduled($hook)) {
130 + wp_schedule_single_event(time(), $hook);
131 + }
132 + return null;
133 + }
134 +
135 + $blocked = null;
136 + $filename = 'fbs-access-check-' . wp_generate_uuid4() . '.txt';
137 + $path = trailingslashit($directory) . $filename;
138 + $contents = wp_generate_uuid4();
139 + try {
140 + if (is_dir($directory) && is_writable($directory)
141 + && file_put_contents($path, $contents, LOCK_EX) === strlen($contents)) {
142 + // The upload URL comes from trusted site configuration, never request input.
143 + $response = wp_remote_get($url . '/' . $filename, [
144 + 'timeout' => 5,
145 + 'redirection' => 0,
146 + 'limit_response_size' => 1024,
147 + 'headers' => ['Cache-Control' => 'no-cache'],
148 + ]);
149 + if (!is_wp_error($response)) {
150 + $status = wp_remote_retrieve_response_code($response);
151 + if (in_array($status, [403, 404], true)) {
152 + $blocked = true;
153 + } elseif ($status === 200 && wp_remote_retrieve_body($response) === $contents) {
154 + $blocked = false;
155 + }
156 + }
157 + }
158 + } finally {
159 + if (is_file($path)) {
160 + wp_delete_file($path);
161 + }
162 + }
163 +
164 + set_transient($cacheKey, ['rules_hash' => $rulesHash, 'blocked' => $blocked], 5 * MINUTE_IN_SECONDS);
165 + return $blocked;
166 + }
167 +
168 + /**
58 169 * Get absolute path of file using custom upload dir name of this application
59 170 * @return string [file path]
60 171 */
61 172 public function _getAbsolutePathOfFile($file)
@@ -133,9 +244,9 @@
133 244 return strpos($filePath, $allowedRoot) === 0 ? $filePath : null;
134 245 }
135 246
136 247 /**
137 - * Upload files into custom upload dir of this application
248 + * Upload request files into the application's custom upload directory.
138 249 * @return array
139 250 */
140 251 public function _uploadFromRequest()
141 252 {
@@ -142,14 +253,18 @@
142 253 return $this->_put(FluentBoards('request')->files());
143 254 }
144 255
145 256 /**
146 - * Upload files into custom upload dir of this application
257 + * Prepare upload-directory protection before accepting local files.
147 258 * @param array $files
148 259 * @return array
149 260 */
150 261 public function _put($files)
151 262 {
263 + if (!$this->_installUploadProtectionRules()) {
264 + throw new Exception(__('Could not prepare upload-directory protection. Please ask your hosting provider to block direct file access and check directory permissions.', 'fluent-boards'));
265 + }
266 +
152 267 if (!function_exists('wp_handle_upload')) {
153 268 require_once(ABSPATH . 'wp-admin/includes/file.php');
154 269 }
155 270