PluginProbe
Patchstack – WordPress & Plugins Security / 2.3.6
Patchstack – WordPress & Plugins Security v2.3.6
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 +270 -74 2.1.12.3.6 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', true );
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,87 @@
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, true );
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', true );
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 ) {
144 +
145 + // Entries that we don't want to store on the API side.
146 + if ( stripos( $item->request_uri, 'wp-comments-post' ) !== false ) {
147 + continue;
148 + }
149 +
150 + // Push to entries to be uploaded.
151 + $logs[] = [
152 + 'ip' => $item->ip,
153 + 'fid' => $item->fid,
154 + 'request_uri' => $item->request_uri,
155 + 'user_agent' => $item->user_agent,
156 + 'method' => $item->method,
157 + 'log_date' => $item->log_date,
158 + 'post_data' => $item->post_data,
159 + ];
96 160
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,
161 + $lastId = $item->id;
162 + }
163 +
164 + // JSON encode the logs and upload.
165 + $logs = json_encode( $logs );
166 + $results = $this->plugin->api->upload_firewall_logs(
167 + [
168 + 'logs' => $logs,
169 + 'type' => 'firewall',
170 + ]
106 171 );
107 - }
108 172
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;
173 + if ( isset( $results['errors'] ) ) {
174 + update_option( 'patchstack_firewall_log_lastid', $successId );
175 + break;
176 + }
177 +
178 + $successId = $lastId;
179 + update_option( 'patchstack_firewall_log_lastid', $successId );
119 180 }
120 181
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 );
182 + // Delete the logs.
183 + $wpdb->query( 'DELETE FROM ' . $wpdb->prefix . 'patchstack_firewall_log WHERE id <= ' . (int) $successId );
124 184
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)' );
185 + // No longer processing.
186 + update_option( 'patchstack_firewall_log_processing', false );
127 187 }
128 188
129 189 /**
130 190 * Synchronize the activity logs with our API.
@@ -133,8 +193,20 @@
133 193 */
134 194 public function upload_activity_logs() {
135 195 global $wpdb;
136 196
197 + // Do not execute upload action on free sites.
198 + if ( ! $this->license_is_active() || $this->get_option( 'patchstack_license_free', 0 ) == 1 ) {
199 + return;
200 + }
201 +
202 + // Do not process if we are already processing a previous batch.
203 + if ( get_option( 'patchstack_eventlog_processing', false ) ) {
204 + return;
205 + }
206 +
207 + update_option( 'patchstack_eventlog_processing', true );
208 +
137 209 // Determine if we should upload failed logins to the app.
138 210 $where = " AND action != 'failed login' ";
139 211 if ( $this->get_option( 'patchstack_activity_log_failed_logins_db', 0 ) == 1 ) {
140 212 $where = ' ';
@@ -139,28 +211,49 @@
139 211 if ( $this->get_option( 'patchstack_activity_log_failed_logins_db', 0 ) == 1 ) {
140 212 $where = ' ';
141 213 }
142 214
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 - }
215 + // Attempt to fetch data, if any.
216 + $lastId = get_option( 'patchstack_eventlog_lastid', 0 );
217 + $successId = $lastId;
149 218
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;
219 + // Do a maximum of several hundred log entries per cronjob.
220 + for ($i = 0; $i <= 1; $i++) {
221 + // Pull the data from the database, in batches of 100.
222 + $items = $wpdb->get_results(
223 + $wpdb->prepare(
224 + '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',
225 + $lastId
226 + ),
227 + ARRAY_A
228 + );
229 +
230 + // No need to continue if we have no data.
231 + if ( $wpdb->num_rows == 0 ) {
232 + update_option( 'patchstack_eventlog_lastid', 0 );
233 + break;
234 + }
235 +
236 + // Get the last ID in the result set.
237 + $lastId = $items[count($items) - 1]['id'];
238 +
239 + // Send to the API.
240 + $logs = json_encode( $items );
241 + $results = $this->plugin->api->upload_activity_logs( [ 'logs' => $logs ] );
242 + if ( isset( $results['errors'] ) ) {
243 + update_option( 'patchstack_eventlog_lastid', $successId );
244 + break;
245 + }
246 +
247 + $successId = $lastId;
248 + update_option( 'patchstack_eventlog_lastid', $successId );
155 249 }
156 250
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 );
251 + // Delete the logs.
252 + $wpdb->query( 'DELETE FROM ' . $wpdb->prefix . 'patchstack_event_log WHERE id <= ' . (int) $successId );
160 253
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)' );
254 + // No longer processing.
255 + update_option( 'patchstack_eventlog_processing', false );
163 256 }
164 257
165 258 /**
166 259 * Obtain information about the software that the user has installed.
@@ -176,9 +269,9 @@
176 269 require_once ABSPATH . 'wp-admin/includes/update.php';
177 270 }
178 271
179 272 // Refetch updates data if we are performing a plugin listener related action.
180 - if ( isset( $_POST['webarx_secret'] ) ) {
273 + if ( isset( $_POST['patchstack_secret'] ) ) {
181 274 @require_once ABSPATH . 'wp-includes/update.php';
182 275 @wp_update_themes();
183 276 @wp_update_plugins();
184 277 }
@@ -186,9 +279,9 @@
186 279 // Fetch list of plugins.
187 280 $all_plugin = get_plugins();
188 281 $installed_plugins = array_keys( $all_plugin );
189 282 $updatable_plugins = get_plugin_updates();
190 - $software_list = array();
283 + $software_list = [];
191 284
192 285 foreach ( $installed_plugins as $plugin ) {
193 286 if ( ! file_exists( WP_PLUGIN_DIR . '/' . $plugin ) ) {
194 287 continue;
@@ -199,16 +292,24 @@
199 292 $plugin_name = empty( $plugin_data['Name'] ) ? '' : $plugin_data['Name'];
200 293 $plugin_version = empty( $plugin_data['Version'] ) ? '' : $plugin_data['Version'];
201 294
202 295 if ( ! empty( $plugin_name ) && ! empty( $plugin_version ) ) {
203 - $software_list[] = array(
296 +
297 + // Determine the active state.
298 + if ( isset( $_GET['action'], $_GET['plugin'] ) && $_GET['action'] == 'deactivate' && $_GET['plugin'] == $plugin) {
299 + $active = 0;
300 + } else {
301 + $active = (int) is_plugin_active( $plugin );
302 + }
303 +
304 + $software_list[] = [
204 305 'sw_type' => 'plugin',
205 306 'sw_name' => $plugin_name,
206 307 'sw_cur_ver' => $plugin_version,
207 308 'sw_new_ver' => $new_version,
208 309 'sw_key' => $plugin,
209 - 'sw_active' => is_plugin_active( $plugin ),
210 - );
310 + 'sw_active' => $active
311 + ];
211 312 }
212 313 }
213 314
214 315 // Fetch list of themes.
@@ -223,15 +324,15 @@
223 324 $theme_name = $themes_data->get( 'Name' );
224 325 $theme_version = $themes_data->get( 'Version' );
225 326
226 327 if ( ! empty( $theme_name ) && ! empty( $theme_version ) ) {
227 - $software_list[] = array(
328 + $software_list[] = [
228 329 'sw_type' => 'theme',
229 330 'sw_name' => $theme_name,
230 331 'sw_cur_ver' => $theme_version,
231 332 'sw_new_ver' => $theme_new_version,
232 333 'sw_key' => $theme_key,
233 - );
334 + ];
234 335 }
235 336 }
236 337
237 338 // Fetch WordPress version.
@@ -237,22 +338,117 @@
237 338 // Fetch WordPress version.
238 339 global $wp_version;
239 340 $core_updates = get_core_updates();
240 341 $new_wp_version = ( ! empty( $core_updates ) && $core_updates[0]->response == 'upgrade' ) ? $core_updates[0]->version : '';
241 - $software_list[] = array(
342 + $software_list[] = [
242 343 'sw_type' => 'wordpress',
243 344 'sw_name' => 'WordPress',
244 345 'sw_cur_ver' => $wp_version,
245 346 'sw_new_ver' => $new_wp_version,
246 - );
347 + ];
247 348
248 349 // Fetch PHP version.
249 - $software_list[] = array(
350 + $software_list[] = [
250 351 'sw_type' => 'php',
251 352 'sw_name' => 'PHP',
252 - 'sw_cur_ver' => substr( phpversion(), 0, 5 ),
353 + 'sw_cur_ver' => phpversion(),
253 354 'sw_new_ver' => '',
254 - );
355 + ];
255 356
357 + // Fetch database server version.
358 + global $wpdb;
359 + if ( ! is_null( $wpdb ) ) {
360 + $software_list[] = [
361 + 'sw_type' => 'database',
362 + 'sw_name' => 'Database',
363 + 'sw_cur_ver' => $wpdb->get_var( 'SELECT VERSION()' ),
364 + 'sw_new_ver' => ''
365 + ];
366 + }
367 +
368 + $software_list = apply_filters( 'patchstack_get_software_data', $software_list );
256 369 return $software_list;
370 + }
371 +
372 + /**
373 + * Import the logs generated by the auto prepend firewall rules.
374 + *
375 + * @return void
376 + */
377 + public function import_ap_logs()
378 + {
379 + if ( ! get_option( 'patchstack_firewall_ap_enabled', false ) ) {
380 + return;
381 + }
382 +
383 + // Do not process if we are already processing a previous batch.
384 + if ( get_option( 'patchstack_firewall_log_ap_processing', false ) ) {
385 + return;
386 + }
387 +
388 + update_option( 'patchstack_firewall_log_ap_processing', true );
389 +
390 + // Attempt to load config file.
391 + $logs = __DIR__ . '/../../../pslogs/logs.php';
392 + if ( ! file_exists( $logs ) ) {
393 + return;
394 + }
395 +
396 + // Load the extension.
397 + if ( ! file_exists( __DIR__ . '/../lib/patchstack/vendor/autoload.php' ) ) {
398 + return;
399 + }
400 +
401 + global $wpdb;
402 +
403 + require_once __DIR__ . '/../lib/patchstack/vendor/autoload.php';
404 + $extension = new Patchstack\Extensions\WordPress\Extension( [], $this );
405 +
406 + // Read the logs file.
407 + $file = new SplFileObject( $logs );
408 +
409 + // Iterate through each line.
410 + while ( ! $file->eof() ) {
411 + $line = $file->fgets();
412 +
413 + // Skip first line.
414 + if ( trim($line) == '' || strpos( $line, '<?php' ) !== false ) {
415 + continue;
416 + }
417 +
418 + // Decode the line to import.
419 + $data = json_decode( base64_decode( $line ), true );
420 + if ( ! $data || ! is_array( $data ) ) {
421 + continue;
422 + }
423 +
424 + // Insert into the logs.
425 + $wpdb->insert(
426 + $wpdb->get_blog_prefix( $data['site_id'] ) . 'patchstack_firewall_log',
427 + [
428 + 'ip' => $data['ip'],
429 + 'request_uri' => $data['request_uri'],
430 + 'user_agent' => $data['user_agent'],
431 + 'method' => $data['method'],
432 + 'fid' => $data['fid'],
433 + 'flag' => '',
434 + 'post_data' => $data['post_data'],
435 + 'block_type' => 'BLOCK'
436 + ]
437 + );
438 +
439 + // Update counters.
440 + $hits = (int) $this->get_blog_option( $data['site_id'], 'patchstack_hits_all_time', 0 );
441 + $this->update_blog_option( $data['site_id'], 'patchstack_hits_all_time', $hits + 1 );
442 +
443 + $counters = $this->get_blog_option( $data['site_id'], 'patchstack_hits_last_30', [] );
444 + $counters = $extension->merge_counters( [ date('Y-m-d') => 1 ], $counters );
445 + $this->update_blog_option( $data['site_id'], 'patchstack_hits_last_30', $counters );
446 + }
447 +
448 + $file = null;
449 + file_put_contents( $logs, '<?php exit; ?>' . PHP_EOL );
450 +
451 + // Update processing state.
452 + update_option( 'patchstack_firewall_log_ap_processing', false );
257 453 }
258 454 }