PluginProbe
Patchstack – WordPress & Plugins Security / 2.3.7
Patchstack – WordPress & Plugins Security v2.3.7
2.3.7 trunk 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.16 2.1.17 2.1.18 2.1.19 2.1.2 2.1.20 2.1.21 2.1.22 2.1.23 2.1.24 2.1.25 2.1.3 2.1.4 2.1.5 2.1.6 All 49 releases
patchstack / includes / upload.php

upload.php in Patchstack – WordPress & Plugins Security 2.3.7, at includes/upload.php

468 lines 15.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 // Do not allow the file to be called directly.
4 if ( ! defined( 'ABSPATH' ) ) {
5 exit;
6 }
7
8 /**
9 * This class is used to upload the local logs to our API so it can
10 * be shown on the app.
11 */
12 class P_Upload extends P_Core {
13
14 /**
15 * Add the actions required to upload logs to our API.
16 *
17 * @param Patchstack $core
18 * @return void
19 */
20 public function __construct( $core ) {
21 parent::__construct( $core );
22
23 // In case the software has never been synchronized, force it.
24 if ( ! get_option( 'patchstack_software_data_hash', false ) && ! get_option( 'patchstack_software_upload_attempted', false ) ) {
25 $this->upload_software();
26 }
27
28 // Register the actions.
29 add_action( 'patchstack_send_software_data', [ $this, 'upload_software' ] );
30 add_action( 'patchstack_send_hacker_logs', [ $this, 'upload_firewall_logs' ] );
31 add_action( 'patchstack_send_event_logs', [ $this, 'upload_activity_logs' ] );
32 add_action( 'patchstack_import_ap_logs', [ $this, 'import_ap_logs' ] );
33
34 // In case a plugin or upgrade has been performed, re-synchronize with the app.
35 add_action( 'activated_plugin', [ $this, 'upload_software' ] );
36 add_action( 'deactivated_plugin', [ $this, 'upload_software' ] );
37 add_action( 'deleted_plugin', [ $this, 'upload_software' ] );
38 add_action( 'upgrader_process_complete', [ $this, 'upload_software' ] );
39 add_action( '_core_updated_successfully', [ &$this, 'upload_software' ] );
40 }
41
42 /**
43 * Synchronize the software data with our API.
44 * This includes plugins, themes, WordPress and PHP version.
45 *
46 * @return void|array
47 */
48 public function upload_software() {
49 // Get the software data and hash.
50 $data = $this->get_software_data();
51 $hash = sha1( json_encode( $data ) );
52
53 // Do not sync for no reason.
54 if ( ! defined( 'DOING_CRON' ) && ! isset( $_POST['patchstack_secret'] ) && get_option( 'patchstack_software_data_hash', false ) === $hash && ! is_admin() && ! defined( 'WP_CLI' ) ) {
55 return;
56 }
57
58 // Make sure to not keep calling this function.
59 update_option( 'patchstack_software_upload_attempted', 1 );
60
61 // Synchronize the software list with the API.
62 $results = $this->plugin->api->upload_software( [ 'software' => json_encode( $data ) ] );
63 if ( isset( $results['success'] ) ) {
64 update_option( 'patchstack_software_data_hash', $hash );
65
66 // The result will also contain a list of all vulnerable plugins on the site that is returned by the API.
67 // If the auto update setting is enabled for vulnerable plugins, perform the update once the 15 minute
68 // scheduled task "patchstack_update_plugins" is executed.
69 $update = get_site_option( 'patchstack_auto_update', [] );
70 if ( isset( $results['vulnerable'] ) && is_array( $update ) && in_array( 'vulnerable', $update ) ) {
71 update_site_option( 'patchstack_vulnerable_plugins', $results['vulnerable'] );
72 }
73
74 // If we have vulnerable plugins, determine if we had them before and if not, pull latest firewall rules.
75 if ( isset( $results['vulnerable'] ) && count( $results['vulnerable'] ) > 0 ) {
76 $prev = get_site_option( 'patchstack_latest_vulnerable', [] );
77 foreach ( $results['vulnerable'] as $vuln ) {
78 if ( is_array( $prev ) && ! in_array ( $vuln, $prev ) ) {
79 do_action( 'patchstack_post_dynamic_firewall_rules' );
80 break;
81 }
82 }
83
84 update_site_option( 'patchstack_latest_vulnerable', $results['vulnerable'] );
85 } else {
86 update_site_option( 'patchstack_latest_vulnerable', [] );
87 }
88
89 // If we received the number of vulnerable count.
90 if ( isset ( $results['vulnerability_count'], $results['vulnerability_fix_count'] ) ) {
91 update_option( 'patchstack_vulns_present', $results['vulnerability_count'] );
92 update_option( 'patchstack_fixes_present', $results['vulnerability_fix_count'] );
93 }
94
95 return $results;
96 }
97
98 return;
99 }
100
101 /**
102 * Synchronize the firewall logs with our API.
103 *
104 * @return void
105 */
106 public function upload_firewall_logs() {
107 global $wpdb;
108
109 // Do not execute upload action on free sites.
110 if ( ! $this->license_is_active() || $this->get_option( 'patchstack_license_free', 0 ) == 1 ) {
111 return;
112 }
113
114 // Do not process if we are already processing a previous batch.
115 if ( get_option( 'patchstack_firewall_log_processing', false ) ) {
116 return;
117 }
118
119 update_option( 'patchstack_firewall_log_processing', 1 );
120
121 // Attempt to fetch data, if any.
122 $lastId = get_option( 'patchstack_firewall_log_lastid', 0 );
123 $successId = $lastId;
124
125 // Do a maximum of 100 log entries per cronjob.
126 for ($i = 0; $i <= 1; $i++) {
127 // Pull the data from the database, in batches of 100.
128 $items = $wpdb->get_results(
129 $wpdb->prepare(
130 'SELECT id, ip, log_date, request_uri, user_agent, fid, method, post_data FROM ' . $wpdb->prefix . 'patchstack_firewall_log WHERE id > %d ORDER BY id LIMIT 0,100',
131 $lastId
132 )
133 );
134
135 // No need to continue if we have no data.
136 if ( $wpdb->num_rows == 0 ) {
137 update_option( 'patchstack_firewall_log_lastid', 0 );
138 break;
139 }
140
141 // Construct the array to be uploaded to our API.
142 $logs = [];
143 foreach ( $items as $item ) {
144
145 // Always advance the cursor so filtered rows don't stall the batch.
146 $lastId = $item->id;
147
148 // Entries that we don't want to store on the API side.
149 if ( stripos( $item->request_uri, 'wp-comments-post' ) !== false ) {
150 continue;
151 }
152
153 // Push to entries to be uploaded.
154 $logs[] = [
155 'ip' => $item->ip,
156 'fid' => $item->fid,
157 'request_uri' => $item->request_uri,
158 'user_agent' => $item->user_agent,
159 'method' => $item->method,
160 'log_date' => $item->log_date,
161 'post_data' => $item->post_data,
162 ];
163 }
164
165 // JSON encode the logs and upload.
166 $logs = json_encode( $logs );
167 $results = $this->plugin->api->upload_firewall_logs(
168 [
169 'logs' => $logs,
170 'type' => 'firewall',
171 ]
172 );
173
174 // A failed upload returns a status code or null instead of an array;
175 // bail without advancing so the logs are not deleted before they reach the API.
176 if ( ! is_array( $results ) || isset( $results['errors'] ) ) {
177 update_option( 'patchstack_firewall_log_lastid', $successId );
178 break;
179 }
180
181 $successId = $lastId;
182 update_option( 'patchstack_firewall_log_lastid', $successId );
183 }
184
185 // Delete the logs.
186 $wpdb->query( 'DELETE FROM ' . $wpdb->prefix . 'patchstack_firewall_log WHERE id <= ' . (int) $successId );
187
188 // No longer processing.
189 update_option( 'patchstack_firewall_log_processing', 0 );
190 }
191
192 /**
193 * Synchronize the activity logs with our API.
194 *
195 * @return void
196 */
197 public function upload_activity_logs() {
198 global $wpdb;
199
200 // Do not execute upload action on free sites.
201 if ( ! $this->license_is_active() || $this->get_option( 'patchstack_license_free', 0 ) == 1 ) {
202 return;
203 }
204
205 // Do not process if we are already processing a previous batch.
206 if ( get_option( 'patchstack_eventlog_processing', false ) ) {
207 return;
208 }
209
210 update_option( 'patchstack_eventlog_processing', 1 );
211
212 // Determine if we should upload failed logins to the app.
213 $where = " AND action != 'failed login' ";
214 if ( $this->get_option( 'patchstack_activity_log_failed_logins_db', 0 ) == 1 ) {
215 $where = ' ';
216 }
217
218 // Attempt to fetch data, if any.
219 $lastId = get_option( 'patchstack_eventlog_lastid', 0 );
220 $successId = $lastId;
221
222 // Do a maximum of several hundred log entries per cronjob.
223 for ($i = 0; $i <= 1; $i++) {
224 // Pull the data from the database, in batches of 100.
225 $items = $wpdb->get_results(
226 $wpdb->prepare(
227 'SELECT id, author, ip, object, object_id, object_name, action, date FROM ' . $wpdb->prefix . 'patchstack_event_log WHERE id > %d' . $where . 'ORDER BY id LIMIT 0,100',
228 $lastId
229 ),
230 ARRAY_A
231 );
232
233 // No need to continue if we have no data.
234 if ( $wpdb->num_rows == 0 ) {
235 update_option( 'patchstack_eventlog_lastid', 0 );
236 break;
237 }
238
239 // Get the last ID in the result set.
240 $lastId = $items[count($items) - 1]['id'];
241
242 // Send to the API.
243 $logs = json_encode( $items );
244 $results = $this->plugin->api->upload_activity_logs( [ 'logs' => $logs ] );
245
246 // A failed upload returns a status code or null instead of an array;
247 // bail without advancing so the logs are not deleted before they reach the API.
248 if ( ! is_array( $results ) || isset( $results['errors'] ) ) {
249 update_option( 'patchstack_eventlog_lastid', $successId );
250 break;
251 }
252
253 $successId = $lastId;
254 update_option( 'patchstack_eventlog_lastid', $successId );
255 }
256
257 // Delete the logs.
258 $wpdb->query( 'DELETE FROM ' . $wpdb->prefix . 'patchstack_event_log WHERE id <= ' . (int) $successId );
259
260 // No longer processing.
261 update_option( 'patchstack_eventlog_processing', 0 );
262 }
263
264 /**
265 * Obtain information about the software that the user has installed.
266 * This includes plugins, themes, WordPress and PHP version.
267 *
268 * @return array
269 */
270 public function get_software_data() {
271 if ( ! function_exists( 'get_plugins' ) ) {
272 require_once ABSPATH . 'wp-admin/includes/plugin.php';
273 }
274 if ( ! function_exists( 'get_plugin_updates' ) ) {
275 require_once ABSPATH . 'wp-admin/includes/update.php';
276 }
277
278 // Refetch updates data if we are performing a plugin listener related action.
279 if ( isset( $_POST['patchstack_secret'] ) ) {
280 @require_once ABSPATH . 'wp-includes/update.php';
281 @wp_update_themes();
282 @wp_update_plugins();
283 }
284
285 // Fetch list of plugins.
286 $all_plugin = get_plugins();
287 $installed_plugins = array_keys( $all_plugin );
288 $updatable_plugins = get_plugin_updates();
289 $software_list = [];
290
291 foreach ( $installed_plugins as $plugin ) {
292 if ( ! file_exists( WP_PLUGIN_DIR . '/' . $plugin ) ) {
293 continue;
294 }
295
296 $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin );
297 $new_version = empty( $updatable_plugins[ $plugin ]->update->new_version ) ? '' : $updatable_plugins[ $plugin ]->update->new_version;
298 $plugin_name = empty( $plugin_data['Name'] ) ? '' : $plugin_data['Name'];
299 $plugin_version = empty( $plugin_data['Version'] ) ? '' : $plugin_data['Version'];
300
301 if ( ! empty( $plugin_name ) && ! empty( $plugin_version ) ) {
302
303 // Determine the active state.
304 if ( isset( $_GET['action'], $_GET['plugin'] ) && $_GET['action'] == 'deactivate' && $_GET['plugin'] == $plugin) {
305 $active = 0;
306 } else {
307 $active = (int) is_plugin_active( $plugin );
308 }
309
310 $software_list[] = [
311 'sw_type' => 'plugin',
312 'sw_name' => $plugin_name,
313 'sw_cur_ver' => $plugin_version,
314 'sw_new_ver' => $new_version,
315 'sw_key' => $plugin,
316 'sw_active' => $active
317 ];
318 }
319 }
320
321 // Fetch list of themes.
322 $themes = wp_get_themes();
323 $themes_keys = array_keys( $themes );
324 $updatable_themes = get_theme_updates();
325
326 foreach ( $themes_keys as $theme_key ) {
327 $themes_data = $themes[ $theme_key ];
328 $theme_temporary = empty( $updatable_themes[ $theme_key ] ) ? '' : $updatable_themes[ $theme_key ];
329 $theme_new_version = empty( $updatable_themes[ $theme_key ] ) || ! isset( $theme_temporary->update, $theme_temporary->update['new_version'] ) ? '' : $theme_temporary->update['new_version'];
330 $theme_name = $themes_data->get( 'Name' );
331 $theme_version = $themes_data->get( 'Version' );
332
333 if ( ! empty( $theme_name ) && ! empty( $theme_version ) ) {
334 $software_list[] = [
335 'sw_type' => 'theme',
336 'sw_name' => $theme_name,
337 'sw_cur_ver' => $theme_version,
338 'sw_new_ver' => $theme_new_version,
339 'sw_key' => $theme_key,
340 ];
341 }
342 }
343
344 // Fetch WordPress version.
345 global $wp_version;
346 $core_updates = get_core_updates();
347 $new_wp_version = ( ! empty( $core_updates ) && $core_updates[0]->response == 'upgrade' ) ? $core_updates[0]->version : '';
348 $software_list[] = [
349 'sw_type' => 'wordpress',
350 'sw_name' => 'WordPress',
351 'sw_cur_ver' => $wp_version,
352 'sw_new_ver' => $new_wp_version,
353 ];
354
355 // Fetch PHP version.
356 $software_list[] = [
357 'sw_type' => 'php',
358 'sw_name' => 'PHP',
359 'sw_cur_ver' => phpversion(),
360 'sw_new_ver' => '',
361 ];
362
363 // Fetch database server version.
364 global $wpdb;
365 if ( ! is_null( $wpdb ) ) {
366 $software_list[] = [
367 'sw_type' => 'database',
368 'sw_name' => 'Database',
369 'sw_cur_ver' => $wpdb->get_var( 'SELECT VERSION()' ),
370 'sw_new_ver' => ''
371 ];
372 }
373
374 $software_list = apply_filters( 'patchstack_get_software_data', $software_list );
375 return $software_list;
376 }
377
378 /**
379 * Import the logs generated by the auto prepend firewall rules.
380 *
381 * @return void
382 */
383 public function import_ap_logs()
384 {
385 if ( ! get_option( 'patchstack_firewall_ap_enabled', false ) ) {
386 return;
387 }
388
389 // Do not process if we are already processing a previous batch.
390 if ( get_option( 'patchstack_firewall_log_ap_processing', false ) ) {
391 return;
392 }
393
394 // Attempt to load config file.
395 $logs = __DIR__ . '/../../../pslogs/logs.php';
396 if ( ! file_exists( $logs ) ) {
397 return;
398 }
399
400 // Load the extension.
401 if ( ! file_exists( __DIR__ . '/../lib/patchstack/vendor/autoload.php' ) ) {
402 return;
403 }
404
405 // Set the processing lock only once we know there is work to do, otherwise an
406 // early return above would leave the lock stuck and block all future imports.
407 update_option( 'patchstack_firewall_log_ap_processing', 1 );
408
409 global $wpdb;
410
411 require_once __DIR__ . '/../lib/patchstack/vendor/autoload.php';
412 $extension = new Patchstack\Extensions\WordPress\Extension( [], $this );
413
414 // Read the logs file.
415 $file = new SplFileObject( $logs );
416
417 // Iterate through each line.
418 while ( ! $file->eof() ) {
419 $line = $file->fgets();
420
421 // Skip first line.
422 if ( trim($line) == '' || strpos( $line, '<?php' ) !== false ) {
423 continue;
424 }
425
426 // Decode the line to import.
427 $data = json_decode( base64_decode( $line ), true );
428 if ( ! $data || ! is_array( $data ) ) {
429 continue;
430 }
431
432 // Skip malformed entries that are missing the fields we rely on below.
433 if ( ! isset( $data['site_id'], $data['ip'], $data['request_uri'], $data['user_agent'], $data['method'], $data['fid'], $data['post_data'] ) ) {
434 continue;
435 }
436
437 // Insert into the logs.
438 $wpdb->insert(
439 $wpdb->get_blog_prefix( $data['site_id'] ) . 'patchstack_firewall_log',
440 [
441 'ip' => $data['ip'],
442 'request_uri' => $data['request_uri'],
443 'user_agent' => $data['user_agent'],
444 'method' => $data['method'],
445 'fid' => $data['fid'],
446 'flag' => '',
447 'post_data' => $data['post_data'],
448 'block_type' => 'BLOCK'
449 ]
450 );
451
452 // Update counters.
453 $hits = (int) $this->get_blog_option( $data['site_id'], 'patchstack_hits_all_time', 0 );
454 $this->update_blog_option( $data['site_id'], 'patchstack_hits_all_time', $hits + 1 );
455
456 $counters = $this->get_blog_option( $data['site_id'], 'patchstack_hits_last_30', [] );
457 $counters = $extension->merge_counters( [ date('Y-m-d') => 1 ], $counters );
458 $this->update_blog_option( $data['site_id'], 'patchstack_hits_last_30', $counters );
459 }
460
461 $file = null;
462 file_put_contents( $logs, '<?php exit; ?>' . PHP_EOL );
463
464 // Update processing state.
465 update_option( 'patchstack_firewall_log_ap_processing', 0 );
466 }
467 }
468