PluginProbe
Patchstack – WordPress & Plugins Security / 2.3.2
Patchstack – WordPress & Plugins Security v2.3.2
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 / listener.php

listener.php in Patchstack – WordPress & Plugins Security 2.3.2, at includes/listener.php

884 lines 26.6 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 communicate from the API to the plugin.
10 */
11 class P_Listener extends P_Core {
12
13 /**
14 * Add the actions required to hide the login page.
15 *
16 * @param Patchstack $core
17 * @return void
18 */
19 public function __construct( $core ) {
20 parent::__construct( $core );
21
22 // Only hook into the action if the authentication is set and valid.
23 if ( isset( $_POST['patchstack_secret'] ) && $this->verifyToken( $_POST['patchstack_secret'] ) ) {
24 add_action( 'init', [ $this, 'handleRequest' ] );
25 }
26
27 // OTT action.
28 if ( isset( $_POST['patchstack_ott_action'] ) ) {
29 $ott = get_option( 'patchstack_ott_action', '' );
30 if ( ! empty( $ott ) && hash_equals( $ott, $_POST['patchstack_ott_action'] ) ) {
31 $this->setIpHeader();
32 }
33 }
34
35 // License (re)activation.
36 if ( isset( $_POST['patchstack_ra_action'] ) ) {
37 $aas = get_option( 'patchstack_activation_secret', '' );
38 $aat = get_option( 'patchstack_activation_time', '' );
39 if ( ! empty( $aas ) && hash_equals( $aas, $_POST['patchstack_ra_action'] ) && ! empty ( $aat ) && ( time() - $aat ) < 1800 ) {
40 $this->setLicenseInfo();
41 }
42 }
43 }
44
45 /**
46 * Handle the incoming request.
47 *
48 * @return void
49 */
50 public function handleRequest() {
51 // Get the request data for the listener action.
52 $request = json_decode(base64_decode($_POST['patchstack_secret']), true);
53 if ( ! isset( $request['data'] ) ) {
54 return;
55 }
56
57 // Parse it; backwards support.
58 $requestData = json_decode( $request['data'], true );
59
60 // Double JSON encoding, edge cases.
61 if ( ! is_array( $requestData ) ) {
62 $requestData = json_decode( $requestData, true );
63 }
64
65 // Failsafe.
66 if ( ! is_array( $requestData ) ) {
67 return;
68 }
69
70 // Set our primary keys.
71 foreach ($requestData as $key => $data) {
72 $_POST[$key] = $data;
73 }
74
75 // Action to execute.
76 $action = $requestData['action'];
77
78 // Available mapped actions.
79 $actions = [
80 'patchstack_remote_users' => 'listUsers',
81 'patchstack_firewall_switch' => 'switchFirewallStatus',
82 'patchstack_wordpress_upgrade' => 'wordpressCoreUpgrade',
83 'patchstack_theme_upgrade' => 'themeUpgrade',
84 'patchstack_plugins_upgrade' => 'pluginsUpgrade',
85 'patchstack_plugins_toggle' => 'pluginsToggle',
86 'patchstack_plugins_delete' => 'pluginsDelete',
87 'patchstack_get_options' => 'getAvailableOptions',
88 'patchstack_set_options' => 'saveOptions',
89 'patchstack_refresh_rules' => 'refreshRules',
90 'patchstack_get_firewall_bans' => 'getFirewallBans',
91 'patchstack_firewall_unban_ip' => 'unbanFirewallIp',
92 'patchstack_firewall_unban_all' => 'unbanFirewallAll',
93 'patchstack_upload_software' => 'uploadSoftware',
94 'patchstack_upload_logs' => 'uploadLogs',
95 'patchstack_send_ping' => 'sendPing',
96 'patchstack_login_bans' => 'getLoginBans',
97 'patchstack_unban_login' => 'unbanLogin',
98 'patchstack_debug_info' => 'debugInfo',
99 'patchstack_set_ip_header' => 'setIpHeader',
100 'patchstack_refresh_license' => 'refreshLicense',
101 'patchstack_reset_2fa' => 'resetTFA',
102 'patchstack_reset_cache' => 'resetCache'
103 ];
104
105 // Action must exist.
106 if ( ! isset( $actions[ $action ] ) ) {
107 return;
108 }
109
110 // Execute the action.
111 call_user_func( [ $this, $actions[ $action ] ] );
112 }
113
114 /**
115 * Determine if the provided secret hash equals the sha1 of the private id and key.
116 *
117 * @param string $secret Hash that is sent from our API.
118 * @return boolean
119 */
120 public function verifyToken( $secret ) {
121 if ( empty ( $secret ) ) {
122 return false;
123 }
124
125 $secret = base64_decode( $secret );
126 $request = json_decode( $secret, true );
127 if ( ! $request || ! isset( $request['nonce'], $request['data'], $request['hmac'], $request['time'] ) ) {
128 return false;
129 }
130
131 // +- 10 minutes.
132 $found = false;
133 for ( $i = -10; $i <= 10; $i++ ) {
134 if ( floor(( time() + ( $i * 30 ) ) / 30 ) == $request['time'] ) {
135 $found = true;
136 }
137 }
138
139 // Timestamp must match.
140 if ( ! $found ) {
141 return false;
142 }
143
144 // Get client id and key.
145 $id = get_option( 'patchstack_clientid' );
146 $key = $this->get_secret_key();
147 if ( empty( $id ) || empty ( $key ) ) {
148 return false;
149 }
150
151 // Compute the hmac.
152 $hmac = hash_hmac( 'sha1', $request['nonce'] . $request['time'] . $request['data'], $key . '-' . $id );
153
154 // Ensure it matches.
155 if ( ! hash_equals( $hmac, $request['hmac'] ) ) {
156 return false;
157 }
158
159 return true;
160 }
161
162 /**
163 * Determine if given action succeded or not, then return the appropriate message.
164 *
165 * @param mixed $thing
166 * @param string $success
167 * @param string $fail
168 * @return void
169 */
170 private function returnResults( $thing, $success = '', $fail = '' ) {
171 if ( ! is_wp_error( $thing ) && $thing !== false ) {
172 wp_send_json( [ 'success' => $success ] );
173 }
174
175 wp_send_json( [ 'error' => $fail ] );
176 }
177
178 /**
179 * Send a ping back to the API.
180 *
181 * @return void
182 */
183 private function sendPing() {
184 do_action( 'patchstack_send_ping' );
185 wp_send_json( [ 'firewall' => $this->get_option( 'patchstack_basic_firewall' ) == 1 ] );
186 }
187
188 /**
189 * Get list of all users on WordPress
190 *
191 * @return void
192 */
193 private function listUsers() {
194 // Only fetch data we actually need.
195 $users = get_users( [ 'role__in' => [ 'administrator', 'editor', 'author', 'contributor' ] ] );
196 $roles = wp_roles();
197 $roles = $roles->get_names();
198 $data = [];
199
200 // Loop through all users.
201 foreach ( $users as $user ) {
202
203 // Get text friendly version of the role.
204 $text = '';
205 foreach ( $user->roles as $role ) {
206 if ( isset( $roles[ $role ] ) ) {
207 $text .= $roles[ $role ] . ', ';
208 } else {
209 $text .= $role . ', ';
210 }
211 }
212
213 // Push to array that we will eventually output.
214 array_push(
215 $data,
216 [
217 'id' => $user->data->ID,
218 'username' => $user->data->user_login,
219 'email' => $user->data->user_email,
220 'roles' => substr( $text, 0, -2 ),
221 ]
222 );
223 }
224
225 wp_send_json( [ 'users' => $data ] );
226 }
227
228 /**
229 * Switch the firewall status from on to off or off to on.
230 *
231 * @return string
232 */
233 private function switchFirewallStatus() {
234 $state = $this->get_option( 'patchstack_basic_firewall' ) == 1;
235 update_option( 'patchstack_basic_firewall', $state == 1 ? 0 : 1, true );
236 $this->returnResults( null, 'Firewall ' . ( $state == 1 ? 'disabled' : 'enabled' ) . '.', null );
237 }
238
239 /**
240 * Upgrade the core of WordPress.
241 *
242 * @return string|void
243 */
244 private function wordpressCoreUpgrade() {
245 @set_time_limit( 180 );
246
247 // Get the core update info.
248 wp_version_check();
249 $core = get_site_transient( 'update_core' );
250
251 // Any updates available?
252 if ( ! isset( $core->updates ) ) {
253 $this->returnResults( false, null, 'No update available at this time.' );
254 }
255
256 // Are we on the latest version already?
257 if ( $core->updates[0]->response == 'latest' ) {
258 $this->returnResults( false, null, 'Site is already running the latest version available.' );
259 }
260
261 // Require some libraries and attempt the upgrade.
262 @include_once ABSPATH . '/wp-admin/includes/admin.php';
263 @include_once ABSPATH . '/wp-admin/includes/class-wp-upgrader.php';
264 $skin = new Automatic_Upgrader_Skin();
265 $upgrader = new Core_Upgrader( $skin );
266 $result = $upgrader->upgrade(
267 $core->updates[0],
268 [
269 'attempt_rollback' => true,
270 'do_rollback' => true,
271 'allow_relaxed_file_ownership' => true,
272 ]
273 );
274 if ( ! $result ) {
275 $this->returnResults( false, null, 'The WordPress core could not be upgraded, most likely because of invalid filesystem connection information.' );
276 }
277
278 // Synchronize again with the API.
279 do_action( 'patchstack_send_software_data' );
280 $this->returnResults( $results, 'WordPress core has been upgraded.' );
281 }
282
283 /**
284 * Upgrade a WordPress theme.
285 *
286 * @return string|void
287 */
288 private function themeUpgrade() {
289 if ( !isset( $_POST['patchstack_theme_upgrade'] ) ) {
290 return;
291 }
292
293 @set_time_limit( 180 );
294
295 // Require some files we need to execute the upgrade.
296 $theme = wp_filter_nohtml_kses( $_POST['patchstack_theme_upgrade'] );
297 @include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
298 if ( file_exists( ABSPATH . 'wp-admin/includes/class-theme-upgrader.php' ) ) {
299 @include_once ABSPATH . 'wp-admin/includes/class-theme-upgrader.php';
300 }
301 @include_once ABSPATH . 'wp-admin/includes/misc.php';
302 @include_once ABSPATH . 'wp-admin/includes/file.php';
303
304 // Upgrade the theme.
305 $skin = new Automatic_Upgrader_Skin();
306 $upgrader = new Theme_Upgrader( $skin );
307 $result = $upgrader->upgrade( $theme, [ 'allow_relaxed_file_ownership' => true ] );
308 if ( ! $result ) {
309 $this->returnResults( false, null, 'The theme could not be upgraded, most likely because of invalid filesystem connection information.' );
310 }
311
312 // Synchronize again with the API.
313 do_action( 'patchstack_send_software_data' );
314 $this->returnResults( null, 'The theme has been updated successfully.' );
315 }
316
317 /**
318 * Upgrade a batch of plugins at once.
319 *
320 * @return string|void
321 */
322 private function pluginsUpgrade() {
323 if (!isset( $_POST['patchstack_plugins_upgrade'] ) ) {
324 return;
325 }
326
327 @set_time_limit( 180 );
328
329 // Must have a valid number of plugins received to upgrade.
330 $plugins = wp_filter_nohtml_kses( $_POST['patchstack_plugins_upgrade'] );
331 $plugins = explode( '|', $plugins );
332 if ( count( $plugins ) == 0 ) {
333 $this->returnResults( false, null, 'No valid plugin names have been given.' );
334 }
335
336 // Require some files we need to execute the upgrade.
337 @include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
338 if ( file_exists( ABSPATH . 'wp-admin/includes/class-plugin-upgrader.php' ) ) {
339 @include_once ABSPATH . 'wp-admin/includes/class-plugin-upgrader.php';
340 }
341 @include_once ABSPATH . 'wp-admin/includes/class-automatic-upgrader-skin.php';
342
343 @include_once ABSPATH . 'wp-admin/includes/plugin.php';
344 @include_once ABSPATH . 'wp-admin/includes/misc.php';
345 @include_once ABSPATH . 'wp-admin/includes/file.php';
346 @include_once ABSPATH . 'wp-admin/includes/template.php';
347 @wp_update_plugins();
348 $all_plugins = get_plugins();
349
350 // New array with all available plugins and the ones we want to upgrade.
351 $upgrade = [];
352 foreach ( $all_plugins as $path => $data ) {
353 $t = explode( '/', $path );
354 if ( in_array( $t[0], $plugins ) ) {
355 array_push( $upgrade, $path );
356 }
357 }
358
359 // Don't continue if we have no valid plugins to upgrade.
360 if ( count( $upgrade ) == 0 ) {
361 $this->returnResults( false, null, 'No valid plugin names have been given.' );
362 }
363
364 // Upgrade the plugins.
365 $skin = new Automatic_Upgrader_Skin();
366 $upgrader = new Plugin_Upgrader( $skin );
367 $result = $upgrader->bulk_upgrade( $upgrade, [ 'allow_relaxed_file_ownership' => true ] );
368 if ( ! $result ) {
369 $this->returnResults( false, null, 'The plugins could not be upgraded, most likely because of invalid filesystem connection information.' );
370 }
371
372 // Synchronize again with the API.
373 do_action( 'patchstack_send_software_data' );
374 $this->returnResults( null, 'The plugins have been updated successfully.' );
375 }
376
377 /**
378 * Toggle the state of a batch of plugin to activated or de-activated.
379 *
380 * @return string|void
381 */
382 private function pluginsToggle() {
383 if (!isset( $_POST['patchstack_plugins'], $_POST['patchstack_plugins_toggle'] ) ) {
384 return;
385 }
386
387 @set_time_limit( 180 );
388
389 // Must have a valid number of plugins received to toggle.
390 $plugins = wp_filter_nohtml_kses( $_POST['patchstack_plugins'] );
391 $plugins = explode( '|', $plugins );
392 $state = $_POST['patchstack_plugins_toggle'] == 'on' ? 'on' : 'off';
393 if ( count( $plugins ) == 0 ) {
394 $this->returnResults( false, null, 'No valid plugin names have been given.' );
395 }
396
397 @include_once ABSPATH . 'wp-admin/includes/plugin.php';
398 $all_plugins = get_plugins();
399
400 // New array with all available plugins and the ones we want to toggle.
401 $toggle = [];
402 foreach ( $all_plugins as $path => $data ) {
403 $t = explode( '/', $path );
404
405 // Don't continue if the plugin does not exist locally.
406 if ( ! in_array( $t[0], $plugins ) ) {
407 continue;
408 }
409
410 // If plugin should be turned on, check if it's already turned on first.
411 if ( $state == 'on' && ! is_plugin_active( $path ) ) {
412 array_push( $toggle, $path );
413 }
414
415 // If plugin should be turned off, check if it's already turned off first.
416 if ( $state == 'off' && is_plugin_active( $path ) ) {
417 array_push( $toggle, $path );
418 }
419 }
420
421 // Don't continue if we have no valid plugins to toggle..
422 if ( count( $toggle ) == 0 ) {
423 $this->returnResults( false, null, 'The plugins are already turned ' . $state . '.' );
424 }
425
426 // Turn the plugins on or off?
427 if ( $state == 'on' ) {
428 activate_plugins( $toggle );
429 }
430
431 if ( $state == 'off' ) {
432 deactivate_plugins( $toggle );
433 }
434
435 // Synchronize again with the API.
436 do_action( 'patchstack_send_software_data' );
437 $this->returnResults( null, 'The ' . ( count( $toggle ) == 1 ? 'plugin has' : 'plugins have' ) . ' been successfully turned ' . $state . '.' );
438 }
439
440 /**
441 * Delete a batch of plugins.
442 *
443 * @return string|void
444 */
445 private function pluginsDelete() {
446 if (!isset( $_POST['patchstack_plugins'] ) ) {
447 return;
448 }
449
450 @set_time_limit( 180 );
451
452 // Must have a valid number of plugins received to toggle.
453 $plugins = wp_filter_nohtml_kses( $_POST['patchstack_plugins'] );
454 $plugins = explode( '|', $plugins );
455 if ( count( $plugins ) == 0 ) {
456 $this->returnResults( false, null, 'No valid plugin names have been given.' );
457 }
458
459 @include_once ABSPATH . 'wp-admin/includes/file.php';
460 @include_once ABSPATH . 'wp-admin/includes/plugin.php';
461 $all_plugins = get_plugins();
462
463 // New array with all available plugins and the ones we want to toggle.
464 $delete = [];
465 foreach ( $all_plugins as $path => $data ) {
466 $t = explode( '/', $path );
467
468 // Don't continue if the plugin does not exist locally.
469 if ( ! in_array( $t[0], $plugins ) ) {
470 continue;
471 }
472
473 array_push( $delete, $path );
474 }
475
476 // Don't continue if we have no valid plugins to toggle..
477 if ( count( $delete ) == 0 ) {
478 $this->returnResults( false, null, 'No valid plugins to delete.' );
479 }
480
481 @deactivate_plugins( $delete );
482 @delete_plugins( $delete );
483
484 // Synchronize again with the API.
485 do_action( 'patchstack_send_software_data' );
486 $this->returnResults( null, 'The plugins have been successfully deleted.' );
487 }
488
489 /**
490 * Save received options.
491 *
492 * @return void
493 */
494 private function saveOptions() {
495 if ( ! isset( $_POST['patchstack_set_options'], $_POST['patchstack_secret'] ) ) {
496 exit;
497 }
498
499 // Get the received options.
500 $options = json_decode( base64_decode( $_POST['patchstack_set_options'] ), true );
501 if ( ! $options || count( $options ) == 0 ) {
502 exit;
503 }
504
505 // Loop through the options and update their value.
506 $exclude_filter = ['patchstack_firewall_custom_rules'];
507 foreach ( $options as $key => $value ) {
508 if ( array_key_exists( $key, $this->plugin->admin_options->options ) ) {
509
510 // Some options should not be filtered and could cause unexpected behavior if they are filtered.
511 if ( ! in_array( $key, $exclude_filter ) ) {
512 $value = map_deep( $value, 'wp_filter_nohtml_kses' );
513 }
514
515 update_option( $key, $value, true );
516 }
517 }
518
519 $this->returnResults( null, 'Plugin options has been updated.' );
520 }
521
522 /**
523 * Return list of keys and values of Patchstack options.
524 *
525 * @return array
526 */
527 private function getAvailableOptions() {
528 // Get all options and filter by the Patchstack prefix.
529 global $wpdb;
530 $options = $wpdb->get_results( "SELECT option_name, option_value FROM " . $wpdb->options . " WHERE option_name LIKE 'patchstack_%'" );
531 $settings = [];
532 $found = [];
533 foreach ( $options as $option ) {
534 array_push( $found, $option->option_name );
535 $settings[] = (array) $option;
536 }
537
538 // Check for potential missing options and add them to the output.
539 foreach( [ 'patchstack_firewall_custom_rules' ] as $slug ) {
540 if ( ! isset ( $found[$slug] ) ) {
541 $settings[] = [
542 'option_name' => $slug,
543 'option_value' => $this->get_option( $slug, '' )
544 ];
545 }
546 }
547
548 // Add custom values which aren't directly available from the options table.
549 // User roles available for whitelisting.
550 $roles = wp_roles();
551 $roles = $roles->get_names();
552 $roles_available = [];
553 foreach ( $roles as $key => $role ) {
554 $roles_available[ $key ] = $role;
555 }
556 $settings[] = [
557 'option_name' => 'patchstack_basic_firewall_roles_available',
558 'option_value' => serialize( $roles_available ),
559 ];
560
561 // Whether or not auto-updates are disabled in the code.
562 $settings[] = [
563 'option_name' => 'patchstack_auto_updates_disabled',
564 'option_value' => defined( 'AUTOMATIC_UPDATER_DISABLED' ) && AUTOMATIC_UPDATER_DISABLED,
565 ];
566
567 wp_send_json( $settings );
568 }
569
570 /**
571 * Pull firewall rules from the API.
572 *
573 * @return void
574 */
575 private function refreshRules() {
576 do_action( 'patchstack_post_dynamic_firewall_rules' );
577 $this->returnResults( null, 'Firewall rules have been refreshed.' );
578 }
579
580 /**
581 * Get a list of IP addresses that are currently banned by the firewall.
582 *
583 * @return void|array
584 */
585 private function getFirewallBans($return = false) {
586 // Calculate block time.
587 $minutes = (int) $this->get_option( 'patchstack_autoblock_minutes', 30 );
588 $timeout = (int) $this->get_option( 'patchstack_autoblock_blocktime', 60 );
589 if ( empty( $minutes ) || empty( $timeout ) ) {
590 $time = 30 + 60;
591 } else {
592 $time = $minutes + $timeout;
593 }
594
595 global $wpdb;
596 $results = $wpdb->get_results(
597 $wpdb->prepare( 'SELECT ip FROM ' . $wpdb->prefix . "patchstack_firewall_log WHERE apply_ban = 1 AND log_date >= ('" . current_time( 'mysql' ) . "' - INTERVAL %d MINUTE) GROUP BY ip", [ $time ] ),
598 OBJECT
599 );
600
601 $out = [];
602 foreach ( $results as $result ) {
603 if ( isset( $result->ip ) ) {
604 array_push( $out, $result->ip );
605 }
606 }
607
608 if ($return) {
609 return $out;
610 }
611
612 wp_send_json( $out );
613 }
614
615 /**
616 * Unban a specific IP address from the firewall.
617 *
618 * @return void
619 */
620 private function unbanFirewallIp() {
621 if ( ! isset( $_POST['patchstack_ip'] ) || !filter_var( $_POST['patchstack_ip'], FILTER_VALIDATE_IP ) ) {
622 return;
623 }
624
625 global $wpdb;
626 $wpdb->query( $wpdb->prepare( 'UPDATE ' . $wpdb->prefix . 'patchstack_firewall_log SET apply_ban = 0 WHERE ip = %s', [ $_POST['patchstack_ip'] ] ) );
627 $this->returnResults( null, 'The IP has been unbanned.' );
628 }
629
630 /**
631 * Unban a specific IP address from the firewall.
632 *
633 * @return void
634 */
635 private function unbanFirewallAll() {
636 global $wpdb;
637
638 // Get all banned IP addresses.
639 $ips = $this->getFirewallBans(true);
640 if (count($ips) == 0) {
641 $this->returnResults( null, 'There are no IP addresses to unban.' );
642 }
643
644 // Unban all IP addresses.
645 foreach ($ips as $ip) {
646 $wpdb->query( $wpdb->prepare( 'UPDATE ' . $wpdb->prefix . 'patchstack_firewall_log SET apply_ban = 0 WHERE ip = %s', [ $ip ] ) );
647 }
648
649 $this->returnResults( null, 'All IP addresses has been unbanned.' );
650 }
651
652 /**
653 * Send all current software on the WordPress site to the API.
654 *
655 * @return void
656 */
657 private function uploadSoftware() {
658 do_action( 'patchstack_send_software_data' );
659 $this->returnResults( null, 'The software data has been sent to the API.' );
660 }
661
662 /**
663 * Upload the firewall and activity logs.
664 *
665 * @return void
666 */
667 private function uploadLogs() {
668 do_action( 'patchstack_send_hacker_logs' );
669 do_action( 'patchstack_send_event_logs' );
670 $this->returnResults( null, 'The logs have been sent to the API.' );
671 }
672
673 /**
674 * Get the currently banned IP addresses from the login page.
675 *
676 * @return void
677 */
678 private function getLoginBans() {
679 // Calculate block time.
680 $minutes = (int) $this->get_option( 'patchstack_anti_bruteforce_minutes', 30 );
681 $timeout = (int) $this->get_option( 'patchstack_anti_bruteforce_blocktime', 60 );
682 if ( empty( $minutes ) || empty( $timeout ) ) {
683 $time = 30 + 60;
684 } else {
685 $time = $minutes + $timeout;
686 }
687
688 // Check if X failed login attempts were made.
689 global $wpdb;
690 $results = $wpdb->get_results(
691 $wpdb->prepare( 'SELECT id, ip, date FROM ' . $wpdb->prefix . "patchstack_event_log WHERE action = 'failed login' AND date >= ('" . current_time( 'mysql' ) . "' - INTERVAL %d MINUTE) GROUP BY ip HAVING COUNT(ip) >= %d ORDER BY date DESC", [ $time, $this->get_option( 'patchstack_anti_bruteforce_attempts', 10 ) ] ),
692 OBJECT
693 );
694
695 // Return the banned IP addresses.
696 wp_send_json( [ 'banned' => $results ] );
697 }
698
699 /**
700 * Unban a banned login IP address.
701 *
702 * @return void
703 */
704 private function unbanLogin() {
705 if ( ! isset( $_POST['id'], $_POST['type'] ) || !ctype_digit( $_POST['id'] ) ) {
706 exit;
707 }
708
709 global $wpdb;
710
711 // Unblock the IP; delete the logs of the IP.
712 if ( $_POST['type'] == 'unblock' ) {
713 // First get the IP address to unblock.
714 $result = $wpdb->get_results(
715 $wpdb->prepare( 'SELECT ip FROM ' . $wpdb->prefix . 'patchstack_event_log WHERE id = %d', [ (int) $_POST['id'] ] )
716 );
717
718 // Unblock the IP address.
719 if ( isset( $result[0], $result[0]->ip ) && filter_var( $result[0]->ip, FILTER_VALIDATE_IP ) ) {
720 $wpdb->query(
721 $wpdb->prepare( 'DELETE FROM ' . $wpdb->prefix . 'patchstack_event_log WHERE ip = %s', [ $result[0]->ip ] )
722 );
723 }
724 }
725
726 // Unblock and whitelist the IP.
727 if ( $_POST['type'] == 'unblock_whitelist' ) {
728 // First get the IP address to whitelist.
729 $result = $wpdb->get_results(
730 $wpdb->prepare( 'SELECT ip FROM ' . $wpdb->prefix . 'patchstack_event_log WHERE id = %d', [ (int) $_POST['id'] ] )
731 );
732
733 // Whitelist and unblock the IP address.
734 if ( isset( $result[0], $result[0]->ip ) && filter_var( $result[0]->ip, FILTER_VALIDATE_IP ) ) {
735 update_option( 'patchstack_login_whitelist', $this->get_option( 'patchstack_login_whitelist', '' ) . "\n" . $result[0]->ip );
736 $wpdb->query(
737 $wpdb->prepare( 'DELETE FROM ' . $wpdb->prefix . 'patchstack_event_log WHERE ip = %s', [ $result[0]->ip ] )
738 );
739 }
740 }
741
742 $this->returnResults( null, 'The unban has been processed.' );
743 }
744
745 /**
746 * Get information for debugging purposes.
747 *
748 * @return void
749 */
750 private function debugInfo() {
751 $debug = [
752 'server' => $_SERVER,
753 'php' => phpversion()
754 ];
755
756 wp_send_json( $debug );
757 }
758
759 /**
760 * Try to determine the proper IP address headers.
761 *
762 * @return void
763 */
764 private function setIpHeader() {
765 if ( ! isset( $_POST['ip'] ) ) {
766 return;
767 }
768
769 $ips = ! is_array ( $_POST['ip'] ) ? [ $_POST['ip'] ] : $_POST['ip'];
770
771 // REMOTE_ADDR?
772 foreach ( $ips as $ip ) {
773 if ( isset( $_SERVER['REMOTE_ADDR'] ) && $_SERVER['REMOTE_ADDR'] == $ip ) {
774 update_option( 'patchstack_firewall_ip_header', 'REMOTE_ADDR', true );
775 update_option( 'patchstack_ip_header_computed', 1 );
776 update_option( 'patchstack_ott_action', '' );
777 wp_send_json( [ 'success' => true, 'header' => 'REMOTE_ADDR' ] );
778 }
779 }
780
781 // IP address headers in order of priority.
782 $priority = [ 'REMOTE_ADDR', 'HTTP_CF_CONNECTING_IP', 'HTTP_X_SUCURI_CLIENTIP', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'SUCURI_RIP' ];
783 foreach ( $ips as $ip ) {
784 foreach ( $priority as $header ) {
785 if ( isset( $_SERVER[ $header ] ) && $_SERVER[ $header ] == $ip ) {
786 update_option( 'patchstack_firewall_ip_header', $header, true );
787 update_option( 'patchstack_ip_header_computed', 1 );
788 update_option( 'patchstack_ott_action', '' );
789 wp_send_json( [ 'success' => true, 'header' => $header ] );
790 }
791 }
792 }
793
794 // Still not found? Iterate over all $_SERVER keys.
795 foreach ( $ips as $ip ) {
796 foreach ( $_SERVER as $key => $value ) {
797 if ( $value == $ip ) {
798 update_option( 'patchstack_firewall_ip_header', $key, true );
799 update_option( 'patchstack_ip_header_computed', 1 );
800 update_option( 'patchstack_ott_action', '' );
801 wp_send_json( [ 'success' => true, 'header' => $key ] );
802 }
803 }
804 }
805
806 update_option( 'patchstack_ott_action', '' );
807 wp_send_json( [ 'success' => false, 'header' => 'unknown' ] );
808 }
809
810 /**
811 * Refresh the license and subscription information.
812 *
813 * @return void
814 */
815 private function refreshLicense () {
816 do_action( 'patchstack_update_license_status' );
817 do_action( 'patchstack_send_software_data' );
818 do_action( 'patchstack_post_dynamic_firewall_rules' );
819
820 wp_send_json( array( 'success' => true ) );
821 }
822
823 /**
824 * Set license information.
825 *
826 * @return void
827 */
828 private function setLicenseInfo () {
829 if ( ! isset( $_POST['id'], $_POST['secret'] ) ) {
830 wp_send_json( [ 'success' => false, 'message' => 'Missing required parameters.' ] );
831 }
832
833 $result = $this->plugin->activation->alter_license( $_POST['id'], $_POST['secret'], 'activate' );
834 if ( $result['result'] == 'error' ) {
835 wp_send_json( [ 'success' => false, 'message' => 'The license could not be activated.' ] );
836 }
837
838 update_option( 'patchstack_activation_secret', '' );
839 update_option( 'patchstack_activation_time', '' );
840 wp_send_json( [ 'success' => true ] );
841 }
842
843 /**
844 * Reset the 2FA data all users or specific ones.
845 *
846 * @return void
847 */
848 private function resetTFA () {
849 global $wpdb;
850
851 // Delete by user id if specified.
852 $where = '';
853 $params = [];
854 if (isset($_POST['user_id'])) {
855 $where = 'AND user_id = %d';
856 $params = [$_POST['user_id']];
857 } elseif (isset($_POST['user_name'])) {
858 $where = 'AND user_id IN (SELECT ID FROM ' . $wpdb->users . ' WHERE user_login = %s OR user_email = %s)';
859 $params = [$_POST['user_name'], $_POST['user_name']];
860 }
861
862 // Delete the 2FA data.
863 $wpdb->query(
864 $wpdb->prepare( "
865 DELETE FROM " . $wpdb->usermeta. "
866 WHERE `meta_key` IN ('webarx_2fa_enabled', 'webarx_2fa_secretkey', 'webarx_2fa_secretkey_nonce')
867 " . $where,
868 $params
869 )
870 );
871
872 wp_send_json( [ 'success' => true ] );
873 }
874
875 /**
876 * Reset the WordPress cache.
877 *
878 * @return void
879 */
880 private function resetCache () {
881 wp_send_json( [ 'success' => wp_cache_flush() ] );
882 }
883 }
884