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
← All changes | includes/upload.php +283 -74 2.1.122.3.7 View file →
@@ -20,22 +20,24 @@
20 20 public function __construct( $core ) {
21 21 parent::__construct( $core );
22 22
23 23 // In case the software has never been synchronized, force it.
24 - if ( ! get_option( 'patchstack_software_data_hash', false ) ) {
24 + if ( ! get_option( 'patchstack_software_data_hash', false ) && ! get_option( 'patchstack_software_upload_attempted', false ) ) {
25 25 $this->upload_software();
26 26 }
27 27
28 28 // Register the actions.
29 - add_action( 'patchstack_send_software_data', array( $this, 'upload_software' ) );
30 - add_action( 'patchstack_send_hacker_logs', array( $this, 'upload_firewall_logs' ) );
31 - add_action( 'patchstack_send_event_logs', array( $this, 'upload_activity_logs' ) );
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' ] );
32 33
33 34 // In case a plugin or upgrade has been performed, re-synchronize with the app.
34 - add_action( 'activated_plugin', array( $this, 'upload_software' ) );
35 - add_action( 'deactivated_plugin', array( $this, 'upload_software' ) );
36 - add_action( 'upgrader_process_complete', array( $this, 'upload_software' ) );
37 - add_action( '_core_updated_successfully', array( &$this, 'upload_software' ) );
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' ] );
38 40 }
39 41
40 42 /**
41 43 * Synchronize the software data with our API.
@@ -46,14 +48,19 @@
46 48 public function upload_software() {
47 49 // Get the software data and hash.
48 50 $data = $this->get_software_data();
49 51 $hash = sha1( json_encode( $data ) );
50 - if ( ! isset( $_POST['webarx_secret'] ) && get_option( 'patchstack_software_data_hash', false ) === $hash ) {
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' ) ) {
51 55 return;
52 56 }
53 57
58 + // Make sure to not keep calling this function.
59 + update_option( 'patchstack_software_upload_attempted', 1 );
60 +
54 61 // Synchronize the software list with the API.
55 - $results = $this->plugin->api->upload_software( array( 'software' => json_encode( $data ) ) );
62 + $results = $this->plugin->api->upload_software( [ 'software' => json_encode( $data ) ] );
56 63 if ( isset( $results['success'] ) ) {
57 64 update_option( 'patchstack_software_data_hash', $hash );
58 65
59 66 // The result will also contain a list of all vulnerable plugins on the site that is returned by the API.
@@ -58,13 +65,34 @@
58 65
59 66 // The result will also contain a list of all vulnerable plugins on the site that is returned by the API.
60 67 // If the auto update setting is enabled for vulnerable plugins, perform the update once the 15 minute
61 68 // scheduled task "patchstack_update_plugins" is executed.
62 - $update = get_site_option( 'patchstack_auto_update', array() );
69 + $update = get_site_option( 'patchstack_auto_update', [] );
63 70 if ( isset( $results['vulnerable'] ) && is_array( $update ) && in_array( 'vulnerable', $update ) ) {
64 71 update_site_option( 'patchstack_vulnerable_plugins', $results['vulnerable'] );
65 72 }
66 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 +
67 95 return $results;
68 96 }
69 97
70 98 return;
@@ -76,55 +104,90 @@
76 104 * @return void
77 105 */
78 106 public function upload_firewall_logs() {
79 107 global $wpdb;
80 - $lastid = get_option( 'patchstack_firewall_log_lastid', 0 );
81 - $items = $wpdb->get_results( $wpdb->prepare( 'SELECT ip, log_date, request_uri, user_agent, fid, method, post_data FROM ' . $wpdb->prefix . 'patchstack_firewall_log WHERE id > %d ORDER BY id', $lastid ) );
82 108
83 - // No need to synchronize if there are no new logs present.
84 - if ( $wpdb->num_rows == 0 ) {
109 + // Do not execute upload action on free sites.
110 + if ( ! $this->license_is_active() || $this->get_option( 'patchstack_license_free', 0 ) == 1 ) {
85 111 return;
86 112 }
87 113
88 - // Construct the array to be uploaded to our API.
89 - $logs = array();
90 - foreach ( $items as $item ) {
114 + // Do not process if we are already processing a previous batch.
115 + if ( get_option( 'patchstack_firewall_log_processing', false ) ) {
116 + return;
117 + }
91 118
92 - // Entries that we don't want to store on the API side.
93 - if ( stripos( $item->request_uri, 'wp-comments-post' ) !== false ) {
94 - continue;
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;
95 139 }
140 +
141 + // Construct the array to be uploaded to our API.
142 + $logs = [];
143 + foreach ( $items as $item ) {
96 144
97 - // Push to entries to be uploaded.
98 - $logs[] = array(
99 - 'ip' => $item->ip,
100 - 'fid' => $item->fid,
101 - 'request_uri' => $item->request_uri,
102 - 'user_agent' => $item->user_agent,
103 - 'method' => $item->method,
104 - 'log_date' => $item->log_date,
105 - 'post_data' => $item->post_data,
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 + ]
106 172 );
107 - }
108 173
109 - // JSON encode the logs and upload.
110 - $logs = json_encode( $logs );
111 - $results = $this->plugin->api->upload_firewall_logs(
112 - array(
113 - 'logs' => $logs,
114 - 'type' => 'firewall',
115 - )
116 - );
117 - if ( isset( $results['errors'] ) ) {
118 - return;
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 );
119 183 }
120 184
121 - // Get the most recent id of the logs.
122 - $lastid = $wpdb->get_var( 'SELECT id FROM ' . $wpdb->prefix . 'patchstack_firewall_log ORDER BY id DESC LIMIT 0, 1' );
123 - update_option( 'patchstack_firewall_log_lastid', $lastid );
185 + // Delete the logs.
186 + $wpdb->query( 'DELETE FROM ' . $wpdb->prefix . 'patchstack_firewall_log WHERE id <= ' . (int) $successId );
124 187
125 - // Delete logs that are older than 2 weeks.
126 - $wpdb->query( 'DELETE FROM ' . $wpdb->prefix . 'patchstack_firewall_log WHERE log_date < DATE_SUB(NOW(), INTERVAL 14 DAY)' );
188 + // No longer processing.
189 + update_option( 'patchstack_firewall_log_processing', 0 );
127 190 }
128 191
129 192 /**
130 193 * Synchronize the activity logs with our API.
@@ -133,8 +196,20 @@
133 196 */
134 197 public function upload_activity_logs() {
135 198 global $wpdb;
136 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 +
137 212 // Determine if we should upload failed logins to the app.
138 213 $where = " AND action != 'failed login' ";
139 214 if ( $this->get_option( 'patchstack_activity_log_failed_logins_db', 0 ) == 1 ) {
140 215 $where = ' ';
@@ -139,28 +214,52 @@
139 214 if ( $this->get_option( 'patchstack_activity_log_failed_logins_db', 0 ) == 1 ) {
140 215 $where = ' ';
141 216 }
142 217
143 - // Do we have data to upload?
144 - $lastid = get_option( 'patchstack_eventlog_lastid', 0 );
145 - $items = $wpdb->get_results( $wpdb->prepare( 'SELECT author, ip, object, object_id, object_name, action, date FROM ' . $wpdb->prefix . 'patchstack_event_log WHERE id > %d' . $where . 'ORDER BY id', array( $lastid ) ) );
146 - if ( $wpdb->num_rows == 0 ) {
147 - return;
148 - }
218 + // Attempt to fetch data, if any.
219 + $lastId = get_option( 'patchstack_eventlog_lastid', 0 );
220 + $successId = $lastId;
149 221
150 - // Send to the API.
151 - $logs = json_encode( $items );
152 - $results = $this->plugin->api->upload_activity_logs( array( 'logs' => $logs ) );
153 - if ( isset( $results['errors'] ) ) {
154 - return;
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 );
155 255 }
156 256
157 - // Get the most recent id of the logs.
158 - $lastid = $wpdb->get_var( 'SELECT id FROM ' . $wpdb->prefix . 'patchstack_event_log ORDER BY id DESC LIMIT 0, 1' );
159 - update_option( 'patchstack_eventlog_lastid', $lastid );
257 + // Delete the logs.
258 + $wpdb->query( 'DELETE FROM ' . $wpdb->prefix . 'patchstack_event_log WHERE id <= ' . (int) $successId );
160 259
161 - // Delete logs that are older than 2 weeks.
162 - $wpdb->query( 'DELETE FROM ' . $wpdb->prefix . 'patchstack_event_log WHERE date < DATE_SUB(NOW(), INTERVAL 14 DAY)' );
260 + // No longer processing.
261 + update_option( 'patchstack_eventlog_processing', 0 );
163 262 }
164 263
165 264 /**
166 265 * Obtain information about the software that the user has installed.
@@ -176,9 +275,9 @@
176 275 require_once ABSPATH . 'wp-admin/includes/update.php';
177 276 }
178 277
179 278 // Refetch updates data if we are performing a plugin listener related action.
180 - if ( isset( $_POST['webarx_secret'] ) ) {
279 + if ( isset( $_POST['patchstack_secret'] ) ) {
181 280 @require_once ABSPATH . 'wp-includes/update.php';
182 281 @wp_update_themes();
183 282 @wp_update_plugins();
184 283 }
@@ -186,9 +285,9 @@
186 285 // Fetch list of plugins.
187 286 $all_plugin = get_plugins();
188 287 $installed_plugins = array_keys( $all_plugin );
189 288 $updatable_plugins = get_plugin_updates();
190 - $software_list = array();
289 + $software_list = [];
191 290
192 291 foreach ( $installed_plugins as $plugin ) {
193 292 if ( ! file_exists( WP_PLUGIN_DIR . '/' . $plugin ) ) {
194 293 continue;
@@ -199,16 +298,24 @@
199 298 $plugin_name = empty( $plugin_data['Name'] ) ? '' : $plugin_data['Name'];
200 299 $plugin_version = empty( $plugin_data['Version'] ) ? '' : $plugin_data['Version'];
201 300
202 301 if ( ! empty( $plugin_name ) && ! empty( $plugin_version ) ) {
203 - $software_list[] = array(
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[] = [
204 311 'sw_type' => 'plugin',
205 312 'sw_name' => $plugin_name,
206 313 'sw_cur_ver' => $plugin_version,
207 314 'sw_new_ver' => $new_version,
208 315 'sw_key' => $plugin,
209 - 'sw_active' => is_plugin_active( $plugin ),
210 - );
316 + 'sw_active' => $active
317 + ];
211 318 }
212 319 }
213 320
214 321 // Fetch list of themes.
@@ -223,15 +330,15 @@
223 330 $theme_name = $themes_data->get( 'Name' );
224 331 $theme_version = $themes_data->get( 'Version' );
225 332
226 333 if ( ! empty( $theme_name ) && ! empty( $theme_version ) ) {
227 - $software_list[] = array(
334 + $software_list[] = [
228 335 'sw_type' => 'theme',
229 336 'sw_name' => $theme_name,
230 337 'sw_cur_ver' => $theme_version,
231 338 'sw_new_ver' => $theme_new_version,
232 339 'sw_key' => $theme_key,
233 - );
340 + ];
234 341 }
235 342 }
236 343
237 344 // Fetch WordPress version.
@@ -237,22 +344,124 @@
237 344 // Fetch WordPress version.
238 345 global $wp_version;
239 346 $core_updates = get_core_updates();
240 347 $new_wp_version = ( ! empty( $core_updates ) && $core_updates[0]->response == 'upgrade' ) ? $core_updates[0]->version : '';
241 - $software_list[] = array(
348 + $software_list[] = [
242 349 'sw_type' => 'wordpress',
243 350 'sw_name' => 'WordPress',
244 351 'sw_cur_ver' => $wp_version,
245 352 'sw_new_ver' => $new_wp_version,
246 - );
353 + ];
247 354
248 355 // Fetch PHP version.
249 - $software_list[] = array(
356 + $software_list[] = [
250 357 'sw_type' => 'php',
251 358 'sw_name' => 'PHP',
252 - 'sw_cur_ver' => substr( phpversion(), 0, 5 ),
359 + 'sw_cur_ver' => phpversion(),
253 360 'sw_new_ver' => '',
254 - );
361 + ];
255 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 );
256 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 );
257 466 }
258 467 }