PluginProbe
FluentAuth – The Ultimate Authorization & Security Plugin for WordPress / trunk
FluentAuth – The Ultimate Authorization & Security Plugin for WordPress vtrunk
trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.1.0 2.0.0 2.0.1 2.0.2 2.0.3 2.1.0 2.1.1 2.1.2
fluent-security / app / Http / Controllers / SecurityScanController.php

SecurityScanController.php in FluentAuth – The Ultimate Authorization & Security Plugin for WordPress trunk, at app/Http/Controllers/SecurityScanController.php

343 lines 11.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentAuth\App\Http\Controllers;
4
5 use FluentAuth\App\Helpers\Arr;
6 use FluentAuth\App\Services\IntegrityChecker\Api;
7 use FluentAuth\App\Services\IntegrityChecker\CheckerService;
8 use FluentAuth\App\Services\IntegrityChecker\IntegrityHelper;
9
10 class SecurityScanController
11 {
12 public static function getSettings(\WP_REST_Request $request)
13 {
14 $settings = IntegrityHelper::getSettings();
15
16 if ($settings['last_checked']) {
17 $settings['last_checked_human'] = human_time_diff(strtotime($settings['last_checked']), current_time('timestamp'));
18 }
19
20 return [
21 'settings' => $settings,
22 'ignores' => IntegrityHelper::getIgnoreLists(),
23 ];
24 }
25
26 public static function registerSite(\WP_REST_Request $request)
27 {
28 if ($request->get_param('status') == 'self') {
29 $defaults = [
30 'status' => 'self',
31 'api_id' => '',
32 'api_key' => '',
33 'last_checked' => '',
34 'account_email_id' => '',
35 'is_ok' => 'yes',
36 'auto_scan' => 'no',
37 'scan_interval' => 'daily',
38 'last_report_sent' => ''
39 ];
40
41 IntegrityHelper::saveSettings($defaults);
42
43 return [
44 'message' => __('Your settings has been saved successfully.', 'fluent-security'),
45 ];
46
47 }
48
49 $info = $request->get_param('info');
50
51 if (!is_array($info)) {
52 $info = [];
53 }
54
55 // Validate the data
56 $infoData = [
57 'email' => sanitize_email(Arr::get($info, 'email', '')),
58 'full_name' => sanitize_text_field(Arr::get($info, 'full_name', '')),
59 'api_id' => sanitize_text_field(Arr::get($info, 'api_id', '')),
60 'api_key' => sanitize_text_field(Arr::get($info, 'api_key', ''))
61 ];
62
63 if (!is_email($infoData['email']) || empty($infoData['full_name'])) {
64 return new \WP_Error('invalid_data', __('Please provide a valid email address and full name.', 'fluent-security'), ['status' => 400, 'data' => $infoData]);
65 }
66
67 $status = $request->get_param('status');
68
69 $settings = IntegrityHelper::getSettings();
70 $isConfirmed = false;
71 if ($status == 'unregistered') {
72 $apiId = Api::registerSite($infoData);
73 } else {
74 $infoData['api_id'] = $settings['api_id'];
75 $apiId = Api::confirmSite($infoData);
76 $isConfirmed = true;
77 }
78
79 if (is_wp_error($apiId)) {
80 return $apiId;
81 }
82
83 if ($isConfirmed) {
84 $settings['api_key'] = $infoData['api_key'];
85 $settings['status'] = 'active';
86 } else {
87 $settings['api_id'] = $apiId;
88 $settings['status'] = 'pending';
89 $settings['account_email_id'] = $infoData['email'];
90 }
91
92 IntegrityHelper::saveSettings($settings);
93
94 return [
95 'message' => 'Your site has been successfully registered. Please provide the API token.',
96 'settings' => $settings
97 ];
98
99 }
100
101 public static function scanSite(\WP_REST_Request $request)
102 {
103 $settings = IntegrityHelper::getSettings();
104 $settings['last_checked'] = current_time('mysql');
105 $settings['is_ok'] = 'yes';
106 IntegrityHelper::saveSettings($settings);
107
108 try {
109 $checkerService = new CheckerService();
110 } catch (\Exception $e) {
111 return new \WP_Error('invalid_response', __('An error occurred while scanning the site. If you continously get this error, please reconnect the API.', 'fluent-security'), ['status' => 422, 'data' => $e->getMessage()]);
112 }
113
114 $scanResults = $checkerService->getScanResults(false);
115 $activeChanges = $checkerService->getScanResults(true);
116
117 $hasIssues = array_filter($activeChanges);
118 $settings['last_checked'] = current_time('mysql');
119 if ($hasIssues) {
120 $settings['is_ok'] = 'no';
121 }
122
123 IntegrityHelper::saveSettings($settings);
124
125 return [
126 'scan_results' => $scanResults,
127 'activeChanges' => $activeChanges,
128 'hasIssues' => !!array_filter($scanResults),
129 'willAlert' => !!array_filter($activeChanges)
130 ];
131 }
132
133 public static function toggleIgnore(\WP_REST_Request $request)
134 {
135 $willRemove = $request->get_param('will_remove') == 'yes';
136 $file = $request->get_param('file');
137
138 if (!is_string($file) || empty($file)) {
139 return new \WP_Error('invalid_data', __('Please provide a valid file name.', 'fluent-security'), ['status' => 400, 'data' => $file]);
140 }
141
142 $isFolder = $request->get_param('is_folder') == 'yes';
143
144 $settings = IntegrityHelper::getIgnoreLists();
145
146 if ($isFolder) {
147 $ignoreLists = $settings['folders'];
148 } else {
149 $ignoreLists = $settings['files'];
150 }
151
152 if ($willRemove) {
153 $ignoreLists = array_diff($ignoreLists, [$file]);
154 } else {
155 $ignoreLists[] = $file;
156 }
157
158 if ($isFolder) {
159 $settings['folders'] = array_values(array_unique($ignoreLists));
160 } else {
161 $settings['files'] = array_values(array_unique($ignoreLists));
162 }
163
164 IntegrityHelper::updateIgnoreLists($settings);
165
166 return [
167 'message' => __('Ignore status has been updated.', 'fluent-security'),
168 'lists' => $settings
169 ];
170 }
171
172 public static function viewFileDiff(\WP_REST_Request $request)
173 {
174 $fileConfig = $request->get_param('viewing_file');
175
176 if (!$fileConfig || empty($fileConfig['file']) || empty($fileConfig['status'])) {
177 return new \WP_Error('invalid_data', __('Please provide a valid file name and status.', 'fluent-security'), ['status' => 400, 'data' => $fileConfig]);
178 }
179
180 $file = $fileConfig['file'];
181 $status = $fileConfig['status'];
182 $folder = $fileConfig['folder'];
183
184 $validFolders = ['', 'wp-admin', 'wp-includes', WPINC];
185
186 if (!in_array($folder, $validFolders)) {
187 return new \WP_Error('invalid_data', __('Invalid folder name.', 'fluent-security'), ['status' => 400, 'data' => $fileConfig]);
188 }
189
190 $isInc = $folder == 'wp-includes';
191
192 if ($folder == 'wp-includes') {
193 $folder = WPINC;
194 }
195
196 if ($folder) {
197 // Allow nested paths for wp-admin/wp-includes, realpath() ensures containment
198 $filePath = ABSPATH . $folder . '/' . $file;
199 $expectedDir = realpath(ABSPATH . $folder);
200 } else {
201 // Root folder: strip directory components to prevent traversal
202 $file = basename($file);
203 $filePath = ABSPATH . $file;
204 $expectedDir = realpath(ABSPATH);
205 }
206
207 $realPath = realpath($filePath);
208
209 if (!$realPath || !$expectedDir || strpos($realPath, $expectedDir . DIRECTORY_SEPARATOR) !== 0) {
210 return new \WP_Error('invalid_data', __('This file could not be viewed for security reason.', 'fluent-security'), ['status' => 400, 'data' => $file]);
211 }
212
213 $sensitivePatterns = [
214 'wp-config',
215 '.htaccess',
216 '.env',
217 'debug.log',
218 'error_log',
219 'php_errorlog',
220 '.user.ini',
221 '.php.ini',
222 'php.ini',
223 '.ftpconfig',
224 '.ssh',
225 ];
226
227 $backupExtensions = ['.bak', '.back', '.backup', '.old', '.orig', '.save', '.swp', '.tmp', '.copy', '~'];
228
229 $fileLower = strtolower($file);
230 foreach ($sensitivePatterns as $pattern) {
231 if (strpos($fileLower, $pattern) !== false) {
232 return new \WP_Error('invalid_data', __('This file could not be viewed.', 'fluent-security'), ['status' => 400, 'data' => $file]);
233 }
234 }
235
236 foreach ($backupExtensions as $ext) {
237 if (substr($fileLower, -strlen($ext)) === $ext) {
238 return new \WP_Error('invalid_data', __('This file could not be viewed.', 'fluent-security'), ['status' => 400, 'data' => $file]);
239 }
240 }
241
242 if (!file_exists($filePath)) {
243 return new \WP_Error('invalid_data', __('This file could not be viewed.', 'fluent-security'), ['status' => 400, 'data' => $file]);
244 }
245
246 // check if the file size is greater than 2MB
247
248 $maxFileSize = 2 * 1024 * 1024; // 2MB
249 if (filesize($filePath) > $maxFileSize) {
250 return new \WP_Error('invalid_data', __('This file is too large to be viewed.', 'fluent-security'), ['status' => 400, 'data' => $file]);
251 }
252
253 // check if the file is readable
254 if (!is_readable($filePath)) {
255 return new \WP_Error('invalid_data', __('This file is not readable.', 'fluent-security'), ['status' => 400, 'data' => $file]);
256 }
257
258 // get file content using WP File System API
259
260 require_once(ABSPATH . 'wp-admin/includes/file.php');
261 WP_Filesystem();
262 global $wp_filesystem;
263 $fileContent = $wp_filesystem->get_contents($filePath);
264
265 $remoteContent = '';
266 if ($status == 'modified') {
267 $originalRelativePath = str_replace(ABSPATH, '', $filePath);
268 if ($isInc) {
269 $originalRelativePath = str_replace(WPINC, 'wp-includes', $originalRelativePath);
270 }
271
272 $remoteContent = Api::getFileContentFromGithub($originalRelativePath);
273
274 if (is_wp_error($remoteContent)) {
275 return new \WP_Error('invalid_data', __('Sorry, we could not compare the changes via Github API.', 'fluent-security'), ['status' => 400]);
276 }
277 }
278
279 return [
280 'filePath' => str_replace(ABSPATH, '/', $filePath),
281 'fileContent' => $fileContent,
282 'hasDiff' => !!$remoteContent,
283 'originalFileContent' => $remoteContent,
284 ];
285
286 }
287
288 public static function updateScheduleScan(\WP_REST_Request $request)
289 {
290 $interval = $request->get_param('scan_interval');
291 $enabled = $request->get_param('auto_scan') == 'yes';
292
293 if (!is_string($interval) || empty($interval)) {
294 return new \WP_Error('invalid_data', __('Please provide a valid interval.', 'fluent-security'), ['status' => 400, 'data' => $interval]);
295 }
296
297 $globalSettings = IntegrityHelper::getSettings();
298
299 $globalSettings['auto_scan'] = $enabled ? 'yes' : 'no';
300 $globalSettings['scan_interval'] = $interval == 'hourly' ? 'hourly' : 'daily';
301
302 IntegrityHelper::saveSettings($globalSettings);
303
304 return [
305 'message' => __('Schedule scan has been updated.', 'fluent-security'),
306 'settings' => $globalSettings
307 ];
308 }
309
310 public static function resetIgnores(\WP_REST_Request $request)
311 {
312 IntegrityHelper::updateIgnoreLists([
313 'files' => [],
314 'folders' => []
315 ]);
316
317 return [
318 'message' => __('Ignore lists have been reset successfully.', 'fluent-security')
319 ];
320 }
321
322 public static function resetApi(\WP_REST_Request $request)
323 {
324 Api::disableApi();
325
326 $settings = IntegrityHelper::getSettings();
327
328 $settings['status'] = 'unregistered';
329 $settings['api_id'] = '';
330 $settings['api_key'] = '';
331 $settings['auto_scan'] = 'no';
332 $settings['scan_interval'] = 'daily';
333 $settings['account_email_id'] = '';
334
335 IntegrityHelper::saveSettings($settings);
336
337 return [
338 'message' => __('API has been reset successfully.', 'fluent-security'),
339 'settings' => $settings
340 ];
341 }
342 }
343