PluginProbe
Patchstack – WordPress & Plugins Security / 2.1.11
Patchstack – WordPress & Plugins Security v2.1.11
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 +74 -283 trunk2.1.11 View file →
@@ -20,24 +20,22 @@
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 ) && ! get_option( 'patchstack_software_upload_attempted', false ) ) {
24 + if ( ! get_option( 'patchstack_software_data_hash', false ) ) {
25 25 $this->upload_software();
26 26 }
27 27
28 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' ] );
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' ) );
33 32
34 33 // 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' ] );
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' ) );
40 38 }
41 39
42 40 /**
43 41 * Synchronize the software data with our API.
@@ -48,19 +46,14 @@
48 46 public function upload_software() {
49 47 // Get the software data and hash.
50 48 $data = $this->get_software_data();
51 49 $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' ) ) {
50 + if ( ! isset( $_POST['webarx_secret'] ) && get_option( 'patchstack_software_data_hash', false ) === $hash ) {
55 51 return;
56 52 }
57 53
58 - // Make sure to not keep calling this function.
59 - update_option( 'patchstack_software_upload_attempted', 1 );
60 -
61 54 // Synchronize the software list with the API.
62 - $results = $this->plugin->api->upload_software( [ 'software' => json_encode( $data ) ] );
55 + $results = $this->plugin->api->upload_software( array( 'software' => json_encode( $data ) ) );
63 56 if ( isset( $results['success'] ) ) {
64 57 update_option( 'patchstack_software_data_hash', $hash );
65 58
66 59 // The result will also contain a list of all vulnerable plugins on the site that is returned by the API.
@@ -65,34 +58,13 @@
65 58
66 59 // The result will also contain a list of all vulnerable plugins on the site that is returned by the API.
67 60 // If the auto update setting is enabled for vulnerable plugins, perform the update once the 15 minute
68 61 // scheduled task "patchstack_update_plugins" is executed.
69 - $update = get_site_option( 'patchstack_auto_update', [] );
62 + $update = get_site_option( 'patchstack_auto_update', array() );
70 63 if ( isset( $results['vulnerable'] ) && is_array( $update ) && in_array( 'vulnerable', $update ) ) {
71 64 update_site_option( 'patchstack_vulnerable_plugins', $results['vulnerable'] );
72 65 }
73 66
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 67 return $results;
96 68 }
97 69
98 70 return;
@@ -104,90 +76,55 @@
104 76 * @return void
105 77 */
106 78 public function upload_firewall_logs() {
107 79 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 ) );
108 82
109 - // Do not execute upload action on free sites.
110 - if ( ! $this->license_is_active() || $this->get_option( 'patchstack_license_free', 0 ) == 1 ) {
83 + // No need to synchronize if there are no new logs present.
84 + if ( $wpdb->num_rows == 0 ) {
111 85 return;
112 86 }
113 87
114 - // Do not process if we are already processing a previous batch.
115 - if ( get_option( 'patchstack_firewall_log_processing', false ) ) {
116 - return;
117 - }
88 + // Construct the array to be uploaded to our API.
89 + $logs = array();
90 + foreach ( $items as $item ) {
118 91
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;
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;
139 95 }
140 -
141 - // Construct the array to be uploaded to our API.
142 - $logs = [];
143 - foreach ( $items as $item ) {
144 96
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 - ]
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,
172 106 );
107 + }
173 108
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 );
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;
183 119 }
184 120
185 - // Delete the logs.
186 - $wpdb->query( 'DELETE FROM ' . $wpdb->prefix . 'patchstack_firewall_log WHERE id <= ' . (int) $successId );
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 );
187 124
188 - // No longer processing.
189 - update_option( 'patchstack_firewall_log_processing', 0 );
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)' );
190 127 }
191 128
192 129 /**
193 130 * Synchronize the activity logs with our API.
@@ -196,20 +133,8 @@
196 133 */
197 134 public function upload_activity_logs() {
198 135 global $wpdb;
199 136
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 137 // Determine if we should upload failed logins to the app.
213 138 $where = " AND action != 'failed login' ";
214 139 if ( $this->get_option( 'patchstack_activity_log_failed_logins_db', 0 ) == 1 ) {
215 140 $where = ' ';
@@ -214,52 +139,28 @@
214 139 if ( $this->get_option( 'patchstack_activity_log_failed_logins_db', 0 ) == 1 ) {
215 140 $where = ' ';
216 141 }
217 142
218 - // Attempt to fetch data, if any.
219 - $lastId = get_option( 'patchstack_eventlog_lastid', 0 );
220 - $successId = $lastId;
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 + }
221 149
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 );
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;
255 155 }
256 156
257 - // Delete the logs.
258 - $wpdb->query( 'DELETE FROM ' . $wpdb->prefix . 'patchstack_event_log WHERE id <= ' . (int) $successId );
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 );
259 160
260 - // No longer processing.
261 - update_option( 'patchstack_eventlog_processing', 0 );
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)' );
262 163 }
263 164
264 165 /**
265 166 * Obtain information about the software that the user has installed.
@@ -275,9 +176,9 @@
275 176 require_once ABSPATH . 'wp-admin/includes/update.php';
276 177 }
277 178
278 179 // Refetch updates data if we are performing a plugin listener related action.
279 - if ( isset( $_POST['patchstack_secret'] ) ) {
180 + if ( isset( $_POST['webarx_secret'] ) ) {
280 181 @require_once ABSPATH . 'wp-includes/update.php';
281 182 @wp_update_themes();
282 183 @wp_update_plugins();
283 184 }
@@ -285,9 +186,9 @@
285 186 // Fetch list of plugins.
286 187 $all_plugin = get_plugins();
287 188 $installed_plugins = array_keys( $all_plugin );
288 189 $updatable_plugins = get_plugin_updates();
289 - $software_list = [];
190 + $software_list = array();
290 191
291 192 foreach ( $installed_plugins as $plugin ) {
292 193 if ( ! file_exists( WP_PLUGIN_DIR . '/' . $plugin ) ) {
293 194 continue;
@@ -298,24 +199,16 @@
298 199 $plugin_name = empty( $plugin_data['Name'] ) ? '' : $plugin_data['Name'];
299 200 $plugin_version = empty( $plugin_data['Version'] ) ? '' : $plugin_data['Version'];
300 201
301 202 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[] = [
203 + $software_list[] = array(
311 204 'sw_type' => 'plugin',
312 205 'sw_name' => $plugin_name,
313 206 'sw_cur_ver' => $plugin_version,
314 207 'sw_new_ver' => $new_version,
315 208 'sw_key' => $plugin,
316 - 'sw_active' => $active
317 - ];
209 + 'sw_active' => is_plugin_active( $plugin ),
210 + );
318 211 }
319 212 }
320 213
321 214 // Fetch list of themes.
@@ -330,15 +223,15 @@
330 223 $theme_name = $themes_data->get( 'Name' );
331 224 $theme_version = $themes_data->get( 'Version' );
332 225
333 226 if ( ! empty( $theme_name ) && ! empty( $theme_version ) ) {
334 - $software_list[] = [
227 + $software_list[] = array(
335 228 'sw_type' => 'theme',
336 229 'sw_name' => $theme_name,
337 230 'sw_cur_ver' => $theme_version,
338 231 'sw_new_ver' => $theme_new_version,
339 232 'sw_key' => $theme_key,
340 - ];
233 + );
341 234 }
342 235 }
343 236
344 237 // Fetch WordPress version.
@@ -344,124 +237,22 @@
344 237 // Fetch WordPress version.
345 238 global $wp_version;
346 239 $core_updates = get_core_updates();
347 240 $new_wp_version = ( ! empty( $core_updates ) && $core_updates[0]->response == 'upgrade' ) ? $core_updates[0]->version : '';
348 - $software_list[] = [
241 + $software_list[] = array(
349 242 'sw_type' => 'wordpress',
350 243 'sw_name' => 'WordPress',
351 244 'sw_cur_ver' => $wp_version,
352 245 'sw_new_ver' => $new_wp_version,
353 - ];
246 + );
354 247
355 248 // Fetch PHP version.
356 - $software_list[] = [
249 + $software_list[] = array(
357 250 'sw_type' => 'php',
358 251 'sw_name' => 'PHP',
359 - 'sw_cur_ver' => phpversion(),
252 + 'sw_cur_ver' => substr( phpversion(), 0, 5 ),
360 253 'sw_new_ver' => '',
361 - ];
254 + );
362 255
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 256 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 257 }
467 258 }