PluginProbe
Patchstack – WordPress & Plugins Security / 2.2.11
Patchstack – WordPress & Plugins Security v2.2.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 +168 -73 2.1.62.2.11 View file →
@@ -20,22 +20,23 @@
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 32
33 33 // 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' ) );
34 + add_action( 'activated_plugin', [ $this, 'upload_software' ] );
35 + add_action( 'deactivated_plugin', [ $this, 'upload_software' ] );
36 + add_action( 'deleted_plugin', [ $this, 'upload_software' ] );
37 + add_action( 'upgrader_process_complete', [ $this, 'upload_software' ] );
38 + add_action( '_core_updated_successfully', [ &$this, 'upload_software' ] );
38 39 }
39 40
40 41 /**
41 42 * Synchronize the software data with our API.
@@ -46,14 +47,19 @@
46 47 public function upload_software() {
47 48 // Get the software data and hash.
48 49 $data = $this->get_software_data();
49 50 $hash = sha1( json_encode( $data ) );
50 - if ( ! isset( $_POST['webarx_secret'] ) && get_option( 'patchstack_software_data_hash', false ) === $hash ) {
51 +
52 + // Do not sync for no reason.
53 + if ( ! defined( 'DOING_CRON' ) && ! isset( $_POST['webarx_secret'] ) && get_option( 'patchstack_software_data_hash', false ) === $hash && ! is_admin() && ! defined( 'WP_CLI ' ) ) {
51 54 return;
52 55 }
53 56
57 + // Make sure to not keep calling this function.
58 + update_option( 'patchstack_software_upload_attempted', true );
59 +
54 60 // Synchronize the software list with the API.
55 - $results = $this->plugin->api->upload_software( array( 'software' => json_encode( $data ) ) );
61 + $results = $this->plugin->api->upload_software( [ 'software' => json_encode( $data ) ] );
56 62 if ( isset( $results['success'] ) ) {
57 63 update_option( 'patchstack_software_data_hash', $hash );
58 64
59 65 // The result will also contain a list of all vulnerable plugins on the site that is returned by the API.
@@ -58,13 +64,28 @@
58 64
59 65 // The result will also contain a list of all vulnerable plugins on the site that is returned by the API.
60 66 // If the auto update setting is enabled for vulnerable plugins, perform the update once the 15 minute
61 67 // scheduled task "patchstack_update_plugins" is executed.
62 - $update = get_site_option( 'patchstack_auto_update', array() );
68 + $update = get_site_option( 'patchstack_auto_update', [] );
63 69 if ( isset( $results['vulnerable'] ) && is_array( $update ) && in_array( 'vulnerable', $update ) ) {
64 70 update_site_option( 'patchstack_vulnerable_plugins', $results['vulnerable'] );
65 71 }
66 72
73 + // If we have vulnerable plugins, determine if we had them before and if not, pull latest firewall rules.
74 + if ( isset( $results['vulnerable'] ) && count( $results['vulnerable'] ) > 0 ) {
75 + $prev = get_site_option( 'patchstack_latest_vulnerable', [] );
76 + foreach ( $results['vulnerable'] as $vuln ) {
77 + if ( ! in_array ( $vuln, $prev ) ) {
78 + do_action( 'patchstack_post_dynamic_firewall_rules' );
79 + break;
80 + }
81 + }
82 +
83 + update_site_option( 'patchstack_latest_vulnerable', $results['vulnerable'] );
84 + } else {
85 + update_site_option( 'patchstack_latest_vulnerable', [] );
86 + }
87 +
67 88 return $results;
68 89 }
69 90
70 91 return;
@@ -76,55 +97,82 @@
76 97 * @return void
77 98 */
78 99 public function upload_firewall_logs() {
79 100 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 101
83 - // No need to synchronize if there are no new logs present.
84 - if ( $wpdb->num_rows == 0 ) {
102 + // Do not process if we are already processing a previous batch.
103 + if ( get_option( 'patchstack_firewall_log_processing', false ) ) {
85 104 return;
86 105 }
87 106
88 - // Construct the array to be uploaded to our API.
89 - $logs = array();
90 - foreach ( $items as $item ) {
107 + update_option( 'patchstack_firewall_log_processing', true );
91 108
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;
109 + // Attempt to fetch data, if any.
110 + $lastId = get_option( 'patchstack_firewall_log_lastid', 0 );
111 + $successId = $lastId;
112 +
113 + // Do a maximum of 500 log entries per cronjob.
114 + for ($i = 0; $i <= 4; $i++) {
115 + // Pull the data from the database, in batches of 100.
116 + $items = $wpdb->get_results(
117 + $wpdb->prepare(
118 + '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',
119 + $lastId
120 + )
121 + );
122 +
123 + // No need to continue if we have no data.
124 + if ( $wpdb->num_rows == 0 ) {
125 + update_option( 'patchstack_firewall_log_lastid', 0 );
126 + break;
95 127 }
128 +
129 + // Construct the array to be uploaded to our API.
130 + $logs = [];
131 + foreach ( $items as $item ) {
132 +
133 + // Entries that we don't want to store on the API side.
134 + if ( stripos( $item->request_uri, 'wp-comments-post' ) !== false ) {
135 + continue;
136 + }
137 +
138 + // Push to entries to be uploaded.
139 + $logs[] = [
140 + 'ip' => $item->ip,
141 + 'fid' => $item->fid,
142 + 'request_uri' => $item->request_uri,
143 + 'user_agent' => $item->user_agent,
144 + 'method' => $item->method,
145 + 'log_date' => $item->log_date,
146 + 'post_data' => $item->post_data,
147 + ];
96 148
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,
149 + $lastId = $item->id;
150 + }
151 +
152 + // JSON encode the logs and upload.
153 + $logs = json_encode( $logs );
154 + $results = $this->plugin->api->upload_firewall_logs(
155 + [
156 + 'logs' => $logs,
157 + 'type' => 'firewall',
158 + ]
106 159 );
107 - }
108 160
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;
161 + if ( isset( $results['errors'] ) ) {
162 + update_option( 'patchstack_firewall_log_lastid', $successId );
163 + break;
164 + }
165 +
166 + $successId = $lastId;
167 + update_option( 'patchstack_firewall_log_lastid', $successId );
119 168 }
120 169
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 );
170 + // Delete the logs.
171 + $wpdb->query( 'DELETE FROM ' . $wpdb->prefix . 'patchstack_firewall_log WHERE id <= ' . (int) $successId );
124 172
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)' );
173 + // No longer processing.
174 + update_option( 'patchstack_firewall_log_processing', false );
127 175 }
128 176
129 177 /**
130 178 * Synchronize the activity logs with our API.
@@ -133,8 +181,15 @@
133 181 */
134 182 public function upload_activity_logs() {
135 183 global $wpdb;
136 184
185 + // Do not process if we are already processing a previous batch.
186 + if ( get_option( 'patchstack_eventlog_processing', false ) ) {
187 + return;
188 + }
189 +
190 + update_option( 'patchstack_eventlog_processing', true );
191 +
137 192 // Determine if we should upload failed logins to the app.
138 193 $where = " AND action != 'failed login' ";
139 194 if ( $this->get_option( 'patchstack_activity_log_failed_logins_db', 0 ) == 1 ) {
140 195 $where = ' ';
@@ -139,28 +194,49 @@
139 194 if ( $this->get_option( 'patchstack_activity_log_failed_logins_db', 0 ) == 1 ) {
140 195 $where = ' ';
141 196 }
142 197
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 - }
198 + // Attempt to fetch data, if any.
199 + $lastId = get_option( 'patchstack_eventlog_lastid', 0 );
200 + $successId = $lastId;
149 201
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;
202 + // Do a maximum of several hundred log entries per cronjob.
203 + for ($i = 0; $i <= 4; $i++) {
204 + // Pull the data from the database, in batches of 100.
205 + $items = $wpdb->get_results(
206 + $wpdb->prepare(
207 + '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',
208 + $lastId
209 + ),
210 + ARRAY_A
211 + );
212 +
213 + // No need to continue if we have no data.
214 + if ( $wpdb->num_rows == 0 ) {
215 + update_option( 'patchstack_eventlog_lastid', 0 );
216 + break;
217 + }
218 +
219 + // Get the last ID in the result set.
220 + $lastId = $items[count($items) - 1]['id'];
221 +
222 + // Send to the API.
223 + $logs = json_encode( $items );
224 + $results = $this->plugin->api->upload_activity_logs( [ 'logs' => $logs ] );
225 + if ( isset( $results['errors'] ) ) {
226 + update_option( 'patchstack_eventlog_lastid', $successId );
227 + break;
228 + }
229 +
230 + $successId = $lastId;
231 + update_option( 'patchstack_eventlog_lastid', $successId );
155 232 }
156 233
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 );
234 + // Delete the logs.
235 + $wpdb->query( 'DELETE FROM ' . $wpdb->prefix . 'patchstack_event_log WHERE id <= ' . (int) $successId );
160 236
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)' );
237 + // No longer processing.
238 + update_option( 'patchstack_eventlog_processing', false );
163 239 }
164 240
165 241 /**
166 242 * Obtain information about the software that the user has installed.
@@ -186,9 +262,9 @@
186 262 // Fetch list of plugins.
187 263 $all_plugin = get_plugins();
188 264 $installed_plugins = array_keys( $all_plugin );
189 265 $updatable_plugins = get_plugin_updates();
190 - $software_list = array();
266 + $software_list = [];
191 267
192 268 foreach ( $installed_plugins as $plugin ) {
193 269 if ( ! file_exists( WP_PLUGIN_DIR . '/' . $plugin ) ) {
194 270 continue;
@@ -199,16 +275,24 @@
199 275 $plugin_name = empty( $plugin_data['Name'] ) ? '' : $plugin_data['Name'];
200 276 $plugin_version = empty( $plugin_data['Version'] ) ? '' : $plugin_data['Version'];
201 277
202 278 if ( ! empty( $plugin_name ) && ! empty( $plugin_version ) ) {
203 - $software_list[] = array(
279 +
280 + // Determine the active state.
281 + if ( isset( $_GET['action'], $_GET['plugin'] ) && $_GET['action'] == 'deactivate' && $_GET['plugin'] == $plugin) {
282 + $active = 0;
283 + } else {
284 + $active = (int) is_plugin_active( $plugin );
285 + }
286 +
287 + $software_list[] = [
204 288 'sw_type' => 'plugin',
205 289 'sw_name' => $plugin_name,
206 290 'sw_cur_ver' => $plugin_version,
207 291 'sw_new_ver' => $new_version,
208 292 'sw_key' => $plugin,
209 - 'sw_active' => is_plugin_active( $plugin ),
210 - );
293 + 'sw_active' => $active
294 + ];
211 295 }
212 296 }
213 297
214 298 // Fetch list of themes.
@@ -223,15 +307,15 @@
223 307 $theme_name = $themes_data->get( 'Name' );
224 308 $theme_version = $themes_data->get( 'Version' );
225 309
226 310 if ( ! empty( $theme_name ) && ! empty( $theme_version ) ) {
227 - $software_list[] = array(
311 + $software_list[] = [
228 312 'sw_type' => 'theme',
229 313 'sw_name' => $theme_name,
230 314 'sw_cur_ver' => $theme_version,
231 315 'sw_new_ver' => $theme_new_version,
232 316 'sw_key' => $theme_key,
233 - );
317 + ];
234 318 }
235 319 }
236 320
237 321 // Fetch WordPress version.
@@ -237,22 +321,33 @@
237 321 // Fetch WordPress version.
238 322 global $wp_version;
239 323 $core_updates = get_core_updates();
240 324 $new_wp_version = ( ! empty( $core_updates ) && $core_updates[0]->response == 'upgrade' ) ? $core_updates[0]->version : '';
241 - $software_list[] = array(
325 + $software_list[] = [
242 326 'sw_type' => 'wordpress',
243 327 'sw_name' => 'WordPress',
244 328 'sw_cur_ver' => $wp_version,
245 329 'sw_new_ver' => $new_wp_version,
246 - );
330 + ];
247 331
248 332 // Fetch PHP version.
249 - $software_list[] = array(
333 + $software_list[] = [
250 334 'sw_type' => 'php',
251 335 'sw_name' => 'PHP',
252 - 'sw_cur_ver' => substr( phpversion(), 0, 5 ),
336 + 'sw_cur_ver' => phpversion(),
253 337 'sw_new_ver' => '',
254 - );
338 + ];
339 +
340 + // Fetch database server version.
341 + global $wpdb;
342 + if ( ! is_null( $wpdb ) ) {
343 + $software_list[] = [
344 + 'sw_type' => 'database',
345 + 'sw_name' => 'Database',
346 + 'sw_cur_ver' => $wpdb->get_var( 'SELECT VERSION()' ),
347 + 'sw_new_ver' => ''
348 + ];
349 + }
255 350
256 351 return $software_list;
257 352 }
258 353 }