PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 3.0.0
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v3.0.0
3.0.0 2.11.12 2.11.11 2.11.10 2.11.9 2.11.7 2.11.8 2.11.6 2.11.5 2.11.4 2.11.3 2.11.1 2.11.2 2.11.0 2.10.5 2.10.4 2.10.3 2.10.2 2.10.1 2.10.0 2.9.9 2.9.8 2.9.6 2.9.7 2.9.5 All 88 releases
← All changes | admin/class-admin-ajax.php +162 -425 2.9.73.0.0 View file →
@@ -22,95 +22,18 @@
22 22 * AJAX: Apply preset
23 23 */
24 24 // ajax_apply_preset() is defined in class-admin.php directly (not in this trait)
25 25
26 - /**
27 - * AJAX: Clear lockouts
26 + /*
27 + * ajax_clear_lockouts(), ajax_clear_logs(), ajax_run_scan() and
28 + * ajax_test_headers() live in class-admin.php. Until 2.11.8 this trait
29 + * carried older copies of the four, and PHP runs the method of the class,
30 + * so the copies never ran: a fix written into one of them would have looked
31 + * applied and changed nothing. Removed after the audit of the admin surface
32 + * for 2.11.8 found them.
28 33 */
29 - public function ajax_clear_lockouts() {
30 - check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
31 34
32 - if ( ! current_user_can( 'manage_options' ) ) {
33 - wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
34 - }
35 -
36 - $ip = isset( $_POST['ip'] ) ? sanitize_text_field( wp_unslash( $_POST['ip'] ) ) : '';
37 -
38 - if ( ! empty( $ip ) ) {
39 - // Clear specific IP
40 - $result = $this->database->clear_lockout( $ip );
41 - } else {
42 - // Clear all
43 - $result = $this->database->clear_all_lockouts();
44 - }
45 -
46 - if ( $result ) {
47 - wp_send_json_success( __( 'Lockouts cleared.', 'vigilante' ) );
48 - } else {
49 - wp_send_json_error( __( 'Failed to clear lockouts.', 'vigilante' ) );
50 - }
51 - }
52 -
53 35 /**
54 - * AJAX: Clear logs
55 - */
56 - public function ajax_clear_logs() {
57 - check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
58 -
59 - if ( ! current_user_can( 'manage_options' ) ) {
60 - wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
61 - }
62 -
63 - $result = $this->activity_log->clear_all_logs();
64 -
65 - if ( $result ) {
66 - wp_send_json_success( __( 'Logs cleared.', 'vigilante' ) );
67 - } else {
68 - wp_send_json_error( __( 'Failed to clear logs.', 'vigilante' ) );
69 - }
70 - }
71 -
72 - /**
73 - * AJAX: Run file integrity scan
74 - */
75 - public function ajax_run_scan() {
76 - check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
77 -
78 - if ( ! current_user_can( 'manage_options' ) ) {
79 - wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
80 - }
81 -
82 - try {
83 - if ( ! class_exists( 'Vigilante_File_Integrity' ) ) {
84 - require_once VIGILANTE_PLUGIN_DIR . 'includes/class-file-integrity.php';
85 - }
86 -
87 - if ( ! $this->settings ) {
88 - wp_send_json_error( 'Settings not initialized' );
89 - }
90 -
91 - $activity_log = isset( $this->activity_log ) ? $this->activity_log : null;
92 - $database = isset( $this->database ) ? $this->database : null;
93 -
94 - $file_integrity = new Vigilante_File_Integrity( $this->settings, $database, $activity_log );
95 - $results = $file_integrity->run_scan();
96 -
97 - // Save results for display
98 - update_option( 'vigilante_last_integrity_scan', time() );
99 - update_option( 'vigilante_last_integrity_results', $results );
100 -
101 - wp_send_json_success( array(
102 - 'message' => __( 'Scan completed.', 'vigilante' ),
103 - 'results' => $results,
104 - ) );
105 - } catch ( Exception $e ) {
106 - wp_send_json_error( 'Exception: ' . $e->getMessage() );
107 - } catch ( Error $e ) {
108 - wp_send_json_error( 'PHP Error: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine() );
109 - }
110 - }
111 -
112 - /**
113 36 * AJAX: Approve a critical config file modification
114 37 *
115 38 * Updates the baseline hash for a single critical file (wp-config.php
116 39 * or .htaccess), accepting the current content as legitimate.
@@ -117,9 +40,23 @@
117 40 */
118 41 public function ajax_approve_critical_file() {
119 42 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
120 43
121 - if ( ! current_user_can( 'manage_options' ) ) {
44 + // Both approvable files, wp-config.php and the root .htaccess, belong
45 + // to the whole network, and since 2.11.3 so does the baseline that
46 + // records them. Approving a change to them is a network action, so on
47 + // a network it takes a network administrator: manage_options is held
48 + // by the administrator of every subsite.
49 + // Written with both calls in plain sight, following the recipe in
50 + // native-aeo-pack/trunk/includes/class-robots-txt.php:650, so the
51 + // surface inventory can read the capability. With the name in a
52 + // variable it can only say "check by hand", and an alert that says
53 + // that forever is an alert nobody reads.
54 + $allowed = is_multisite()
55 + ? current_user_can( 'manage_network_options' )
56 + : current_user_can( 'manage_options' );
57 +
58 + if ( ! $allowed ) {
122 59 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
123 60 }
124 61
125 62 // The request carries an opaque key instead of the file name: hosting
@@ -243,8 +180,17 @@
243 180 $log->is_ip_whitelisted = ( '' !== $ip_val && in_array( $ip_val, $ip_whitelist, true ) );
244 181 $log->is_ip_blacklisted = ( '' !== $ip_val && in_array( $ip_val, $ip_blacklist, true ) );
245 182 $log->is_ua_whitelisted = ( '' !== $ua_val && in_array( $ua_val, $ua_whitelist, true ) );
246 183 $log->is_ua_blacklisted = ( '' !== $ua_val && in_array( $ua_val, $ua_blacklist, true ) );
184 + $log->request_uri = Vigilante_Activity_Log::extract_request_uri( $log->extra_data ?? '' );
185 + // Same explanation as the first page load: without this, an entry
186 + // reached by filtering or paginating would open a popup with less
187 + // in it than the same entry opened from the first page.
188 + $log->self_guidance = Vigilante_Self_Integrity_Guidance::for_log_event(
189 + (string) ( $log->event_action ?? '' ),
190 + $log->extra_data ?? '',
191 + (string) ( $log->severity ?? 'info' )
192 + );
247 193 }
248 194
249 195 wp_send_json_success( array(
250 196 'logs' => $logs,
@@ -325,8 +271,18 @@
325 271 $removed_from_opposite = true;
326 272 }
327 273 }
328 274
275 + // On the main site of a network the whitelists also build the .htaccess
276 + // rules every site shares, so a user without network rights cannot put
277 + // an entry in them or take one out (2.11.6).
278 + $locked = Vigilante_Settings::get_locked_file_settings();
279 + $locked_firewall = ( isset( $locked['firewall'] ) && is_array( $locked['firewall'] ) ) ? $locked['firewall'] : array();
280 +
281 + if ( in_array( $option_key, $locked_firewall, true ) || ( $removed_from_opposite && in_array( $opposite_key, $locked_firewall, true ) ) ) {
282 + wp_send_json_error( Vigilante_Settings::get_shared_files_notice() );
283 + }
284 +
329 285 wp_cache_delete( Vigilante_Settings::OPTION_NAME, 'options' );
330 286 update_option( Vigilante_Settings::OPTION_NAME, $all_options );
331 287 $this->settings->clear_cache();
332 288
@@ -375,281 +331,8 @@
375 331 wp_send_json_success( $message );
376 332 }
377 333
378 334 /**
379 - * AJAX: Test security headers
380 - */
381 - public function ajax_test_headers() {
382 - check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
383 -
384 - if ( ! current_user_can( 'manage_options' ) ) {
385 - wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
386 - }
387 -
388 - $security_headers = new Vigilante_Security_Headers( $this->settings );
389 - $results = $security_headers->test_headers();
390 -
391 - wp_send_json_success( $results );
392 - }
393 -
394 - /**
395 - * Sanitize section data
396 - *
397 - * @param string $section Section name.
398 - * @param array $data Data to sanitize.
399 - * @return array Sanitized data.
400 - */
401 - private function sanitize_section_data( $section, $data ) {
402 - $sanitized = array();
403 -
404 - switch ( $section ) {
405 - case 'firewall':
406 - $sanitized = $this->sanitize_firewall_data( $data );
407 - break;
408 -
409 - case 'login_security':
410 - $sanitized = $this->sanitize_login_security_data( $data );
411 - break;
412 -
413 - case 'security_headers':
414 - $sanitized = $this->sanitize_security_headers_data( $data );
415 - break;
416 -
417 - case 'activity_log':
418 - // Activity log uses process_section_data() directly
419 - $sanitized = $this->sanitize_generic_data( $data );
420 - break;
421 -
422 - case 'user_security':
423 - case 'user_security_advanced':
424 - $sanitized = $this->sanitize_user_security_data( $data );
425 - break;
426 -
427 - default:
428 - // Generic sanitization
429 - $sanitized = $this->sanitize_generic_data( $data );
430 - break;
431 - }
432 -
433 - return $sanitized;
434 - }
435 -
436 - /**
437 - * Sanitize user security data
438 - *
439 - * @param array $data Data to sanitize.
440 - * @return array
441 - */
442 - private function sanitize_user_security_data( $data ) {
443 - $user = isset( $data['user_security'] ) ? $data['user_security'] : $data;
444 -
445 - $sanitized = array(
446 - 'block_insecure_usernames' => ! empty( $user['block_insecure_usernames'] ),
447 - 'force_strong_passwords' => ! empty( $user['force_strong_passwords'] ),
448 - 'min_password_length' => isset( $user['min_password_length'] ) ? absint( $user['min_password_length'] ) : 12,
449 - 'block_author_scanning' => ! empty( $user['block_author_scanning'] ),
450 - 'prevent_display_name_login_match' => ! empty( $user['prevent_display_name_login_match'] ),
451 - );
452 -
453 - // Admin monitoring
454 - if ( isset( $user['admin_monitoring'] ) ) {
455 - $sanitized['admin_monitoring'] = array(
456 - 'alert_new_admin' => ! empty( $user['admin_monitoring']['alert_new_admin'] ),
457 - 'alert_admin_email_change' => ! empty( $user['admin_monitoring']['alert_admin_email_change'] ),
458 - 'alert_permission_elevation' => ! empty( $user['admin_monitoring']['alert_permission_elevation'] ),
459 - );
460 - }
461 -
462 - // Registration approval
463 - if ( isset( $user['registration_approval'] ) ) {
464 - $sanitized['registration_approval'] = array(
465 - 'enabled' => ! empty( $user['registration_approval']['enabled'] ),
466 - 'notify_admin' => ! empty( $user['registration_approval']['notify_admin'] ),
467 - 'auto_reject_days' => isset( $user['registration_approval']['auto_reject_days'] )
468 - ? absint( $user['registration_approval']['auto_reject_days'] )
469 - : 0,
470 - 'affected_roles' => isset( $user['registration_approval']['affected_roles'] )
471 - ? array_map( 'sanitize_key', (array) $user['registration_approval']['affected_roles'] )
472 - : array( 'subscriber' ),
473 - );
474 - }
475 -
476 - // Session management
477 - if ( isset( $user['session_management'] ) ) {
478 - $sanitized['session_management'] = array(
479 - 'enabled' => ! empty( $user['session_management']['enabled'] ),
480 - 'show_in_profile' => ! empty( $user['session_management']['show_in_profile'] ),
481 - );
482 - }
483 -
484 - // Session limits
485 - if ( isset( $user['session_limits'] ) ) {
486 - $sanitized['session_limits'] = array(
487 - 'enabled' => ! empty( $user['session_limits']['enabled'] ),
488 - 'max_sessions' => isset( $user['session_limits']['max_sessions'] )
489 - ? absint( $user['session_limits']['max_sessions'] )
490 - : 3,
491 - 'behavior' => isset( $user['session_limits']['behavior'] )
492 - ? sanitize_key( $user['session_limits']['behavior'] )
493 - : 'close_oldest',
494 - 'exclude_admins' => ! empty( $user['session_limits']['exclude_admins'] ),
495 - );
496 - }
497 -
498 - // Password expiration
499 - if ( isset( $user['password_expiration'] ) ) {
500 - $sanitized['password_expiration'] = array(
501 - 'enabled' => ! empty( $user['password_expiration']['enabled'] ),
502 - 'expire_days' => isset( $user['password_expiration']['expire_days'] )
503 - ? absint( $user['password_expiration']['expire_days'] )
504 - : 90,
505 - 'warning_days' => isset( $user['password_expiration']['warning_days'] )
506 - ? absint( $user['password_expiration']['warning_days'] )
507 - : 14,
508 - 'affected_roles' => isset( $user['password_expiration']['affected_roles'] )
509 - ? array_map( 'sanitize_key', (array) $user['password_expiration']['affected_roles'] )
510 - : array( 'administrator', 'editor' ),
511 - 'excluded_users' => isset( $user['password_expiration']['excluded_users'] )
512 - ? array_values( array_unique( array_filter( array_map( 'absint', (array) $user['password_expiration']['excluded_users'] ) ) ) )
513 - : array(),
514 - 'password_history' => isset( $user['password_expiration']['password_history'] )
515 - ? absint( $user['password_expiration']['password_history'] )
516 - : 3,
517 - 'send_reminder' => ! empty( $user['password_expiration']['send_reminder'] ),
518 - );
519 - }
520 -
521 - // Email verification
522 - if ( isset( $user['email_verification'] ) ) {
523 - $sanitized['email_verification'] = array(
524 - 'enabled' => ! empty( $user['email_verification']['enabled'] ),
525 - 'token_expiry_hours' => isset( $user['email_verification']['token_expiry_hours'] )
526 - ? absint( $user['email_verification']['token_expiry_hours'] )
527 - : 24,
528 - 'allow_resend' => ! empty( $user['email_verification']['allow_resend'] ),
529 - 'auto_delete_days' => isset( $user['email_verification']['auto_delete_days'] )
530 - ? absint( $user['email_verification']['auto_delete_days'] )
531 - : 7,
532 - );
533 - }
534 -
535 - return $sanitized;
536 - }
537 -
538 - /**
539 - * Sanitize firewall data
540 - *
541 - * @param array $data Data to sanitize.
542 - * @return array
543 - */
544 - private function sanitize_firewall_data( $data ) {
545 - $firewall = isset( $data['firewall'] ) ? $data['firewall'] : $data;
546 -
547 - $proxy_header = sanitize_text_field( wp_unslash( $firewall['trusted_proxy_header'] ?? '' ) );
548 - if ( ! in_array( $proxy_header, array( 'cf-connecting-ip', 'x-forwarded-for', 'x-real-ip' ), true ) ) {
549 - $proxy_header = '';
550 - }
551 -
552 - return array(
553 - 'block_bad_query_strings' => ! empty( $firewall['block_bad_query_strings'] ),
554 - 'block_sql_injection' => ! empty( $firewall['block_sql_injection'] ),
555 - 'block_xss_attacks' => ! empty( $firewall['block_xss_attacks'] ),
556 - 'block_file_inclusion' => ! empty( $firewall['block_file_inclusion'] ),
557 - 'block_directory_traversal' => ! empty( $firewall['block_directory_traversal'] ),
558 - 'block_php_in_uploads' => ! empty( $firewall['block_php_in_uploads'] ),
559 - 'block_sensitive_files' => ! empty( $firewall['block_sensitive_files'] ),
560 - 'block_bad_bots' => ! empty( $firewall['block_bad_bots'] ),
561 - 'block_empty_user_agent' => ! empty( $firewall['block_empty_user_agent'] ),
562 - 'allowed_http_methods' => isset( $firewall['allowed_http_methods'] )
563 - ? array_map( 'sanitize_text_field', (array) $firewall['allowed_http_methods'] )
564 - : array( 'GET', 'POST', 'HEAD' ),
565 - 'rate_limiting' => array(
566 - 'enabled' => ! empty( $firewall['rate_limiting']['enabled'] ),
567 - 'requests_per_minute' => isset( $firewall['rate_limiting']['requests_per_minute'] )
568 - ? absint( $firewall['rate_limiting']['requests_per_minute'] )
569 - : 120,
570 - 'block_duration' => isset( $firewall['rate_limiting']['block_duration'] )
571 - ? absint( $firewall['rate_limiting']['block_duration'] )
572 - : 300,
573 - 'progressive' => ! empty( $firewall['rate_limiting']['progressive'] ),
574 - 'max_block_duration' => isset( $firewall['rate_limiting']['max_block_duration'] )
575 - ? absint( $firewall['rate_limiting']['max_block_duration'] )
576 - : 86400,
577 - ),
578 - 'ip_whitelist' => $this->sanitize_ip_list( $firewall['ip_whitelist'] ?? '' ),
579 - 'ip_blacklist' => $this->sanitize_ip_list( $firewall['ip_blacklist'] ?? '' ),
580 - 'ua_whitelist' => $this->sanitize_ua_list( $firewall['ua_whitelist'] ?? '' ),
581 - 'ua_blacklist' => $this->sanitize_ua_list( $firewall['ua_blacklist'] ?? '' ),
582 - 'trusted_proxy_header' => $proxy_header,
583 - );
584 - }
585 -
586 - /**
587 - * Sanitize login security data
588 - *
589 - * @param array $data Data to sanitize.
590 - * @return array
591 - */
592 - private function sanitize_login_security_data( $data ) {
593 - $login = isset( $data['login_security'] ) ? $data['login_security'] : $data;
594 -
595 - $sanitized = array(
596 - 'max_attempts' => isset( $login['max_attempts'] ) ? absint( $login['max_attempts'] ) : 5,
597 - 'lockout_duration' => isset( $login['lockout_duration'] ) ? absint( $login['lockout_duration'] ) : 1800,
598 - 'lockout_increment' => ! empty( $login['lockout_increment'] ),
599 - 'max_lockout_duration' => isset( $login['max_lockout_duration'] ) ? absint( $login['max_lockout_duration'] ) : 86400,
600 - 'hide_login_errors' => ! empty( $login['hide_login_errors'] ),
601 - 'disable_xmlrpc' => ! empty( $login['disable_xmlrpc'] ),
602 - 'disable_xmlrpc_pingback' => ! empty( $login['disable_xmlrpc_pingback'] ),
603 - 'disable_application_passwords' => ! empty( $login['disable_application_passwords'] ),
604 - 'notify_on_lockout' => ! empty( $login['notify_on_lockout'] ),
605 - 'notify_on_admin_login' => ! empty( $login['notify_on_admin_login'] ),
606 - 'notify_email' => isset( $login['notify_email'] ) ? sanitize_email( $login['notify_email'] ) : '',
607 - 'ip_whitelist' => $this->sanitize_ip_list( $login['ip_whitelist'] ?? '' ),
608 - );
609 -
610 - // Two-Factor Authentication
611 - if ( isset( $login['two_factor'] ) ) {
612 - $sanitized['two_factor'] = $this->sanitize_two_factor_data( $login['two_factor'] );
613 - }
614 -
615 - return $sanitized;
616 - }
617 -
618 - /**
619 - * Sanitize security headers data
620 - *
621 - * @param array $data Data to sanitize.
622 - * @return array
623 - */
624 - private function sanitize_security_headers_data( $data ) {
625 - $headers = isset( $data['security_headers'] ) ? $data['security_headers'] : $data;
626 -
627 - return array(
628 - 'enabled' => true,
629 - 'x_frame_options' => isset( $headers['x_frame_options'] ) ? sanitize_text_field( $headers['x_frame_options'] ) : 'SAMEORIGIN',
630 - 'x_content_type_options'=> ! empty( $headers['x_content_type_options'] ),
631 - 'referrer_policy' => isset( $headers['referrer_policy'] ) ? sanitize_text_field( $headers['referrer_policy'] ) : 'strict-origin-when-cross-origin',
632 - 'hsts' => array(
633 - 'enabled' => ! empty( $headers['hsts']['enabled'] ),
634 - 'max_age' => isset( $headers['hsts']['max_age'] ) ? absint( $headers['hsts']['max_age'] ) : 31536000,
635 - 'include_subdomains' => ! empty( $headers['hsts']['include_subdomains'] ),
636 - 'preload' => ! empty( $headers['hsts']['preload'] ),
637 - ),
638 - 'csp' => array(
639 - 'enabled' => ! empty( $headers['csp']['enabled'] ),
640 - 'report_only' => ! empty( $headers['csp']['report_only'] ),
641 - 'directives' => isset( $headers['csp']['directives'] )
642 - ? $this->sanitize_csp_directives( $headers['csp']['directives'] )
643 - : array(),
644 - ),
645 - 'permissions_policy' => array(
646 - 'enabled' => ! empty( $headers['permissions_policy']['enabled'] ),
647 - ),
648 - );
649 - }
650 -
651 - /**
652 335 * Sanitize activity log data
653 336 *
654 337 * @param array $data Data to sanitize.
655 338 * @return array
@@ -654,32 +337,8 @@
654 337 * @param array $data Data to sanitize.
655 338 * @return array
656 339 */
657 340 /**
658 - * Sanitize generic data
659 - *
660 - * @param array $data Data to sanitize.
661 - * @return array
662 - */
663 - private function sanitize_generic_data( $data ) {
664 - $sanitized = array();
665 -
666 - foreach ( $data as $key => $value ) {
667 - if ( is_array( $value ) ) {
668 - $sanitized[ $key ] = $this->sanitize_generic_data( $value );
669 - } elseif ( is_bool( $value ) || in_array( $value, array( '0', '1', 0, 1 ), true ) ) {
670 - $sanitized[ $key ] = (bool) $value;
671 - } elseif ( is_numeric( $value ) ) {
672 - $sanitized[ $key ] = absint( $value );
673 - } else {
674 - $sanitized[ $key ] = sanitize_text_field( $value );
675 - }
676 - }
677 -
678 - return $sanitized;
679 - }
680 -
681 - /**
682 341 * Sanitize IP list
683 342 *
684 343 * @param string|array $ips IPs as string (newline separated) or array.
685 344 * @return array
@@ -725,42 +384,8 @@
725 384 return array_unique( $sanitized );
726 385 }
727 386
728 387 /**
729 - * Sanitize CSP directives
730 - *
731 - * NOTE: dead code. Vigilante_Admin declares its own sanitize_section_data(),
732 - * which takes precedence over this trait's, so neither that method nor the
733 - * per-section sanitizers it calls (this one included) are ever reached. The
734 - * live save path is Vigilante_Admin::process_section_data(). Kept as-is
735 - * rather than deleted mid-release; scheduled for removal in 3.0.
736 - *
737 - * @param array $directives CSP directives.
738 - * @return array
739 - */
740 - private function sanitize_csp_directives( $directives ) {
741 - $sanitized = array();
742 - $allowed_directives = array(
743 - 'default-src', 'script-src', 'style-src', 'img-src', 'font-src',
744 - 'connect-src', 'media-src', 'frame-src', 'frame-ancestors',
745 - 'base-uri', 'form-action', 'object-src', 'worker-src',
746 - 'upgrade-insecure-requests',
747 - );
748 -
749 - foreach ( $allowed_directives as $directive ) {
750 - if ( isset( $directives[ $directive ] ) ) {
751 - if ( 'upgrade-insecure-requests' === $directive ) {
752 - $sanitized[ $directive ] = ! empty( $directives[ $directive ] );
753 - } else {
754 - $sanitized[ $directive ] = sanitize_text_field( $directives[ $directive ] );
755 - }
756 - }
757 - }
758 -
759 - return $sanitized;
760 - }
761 -
762 - /**
763 388 * AJAX: Search users for 2FA exclusion
764 389 */
765 390 public function ajax_search_users_2fa() {
766 391 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
@@ -942,21 +567,45 @@
942 567 if ( ! class_exists( 'Vigilante_Two_Factor_TOTP' ) ) {
943 568 require_once VIGILANTE_INCLUDES_DIR . 'class-two-factor-totp.php';
944 569 }
945 570
946 - $totp = new Vigilante_Two_Factor_TOTP( $this->settings, $this->database, $this->activity_log );
947 - $count = 0;
571 + $totp = new Vigilante_Two_Factor_TOTP( $this->settings, $this->database, $this->activity_log );
572 + $count = 0;
573 + $skipped = 0;
948 574
949 575 foreach ( $user_ids as $uid ) {
950 - if ( $uid > 0 ) {
951 - $totp->reset_user_totp( $uid );
952 - $count++;
576 + if ( $uid < 1 ) {
577 + continue;
953 578 }
579 +
580 + // Same gate as the rest of the TOTP handlers: resetting somebody's
581 + // second factor is editing their account, so ask for edit_user
582 + // rather than for manage_options, which on a network is per site.
583 + if ( ! current_user_can( 'edit_user', $uid ) ) {
584 + $skipped++;
585 + continue;
586 + }
587 +
588 + $totp->reset_user_totp( $uid );
589 + $count++;
954 590 }
955 591
592 + $message = sprintf(
593 + /* translators: %d: Number of users reset */
594 + _n( 'TOTP reset for %d user.', 'TOTP reset for %d users.', $count, 'vigilante' ),
595 + $count
596 + );
597 +
598 + if ( $skipped > 0 ) {
599 + $message .= ' ' . sprintf(
600 + /* translators: %d: Number of users skipped because the current user cannot edit them */
601 + __( '%d skipped: you cannot edit those users.', 'vigilante' ),
602 + $skipped
603 + );
604 + }
605 +
956 606 wp_send_json_success( array(
957 - /* translators: %d: Number of users reset */
958 - 'message' => sprintf( _n( 'TOTP reset for %d user.', 'TOTP reset for %d users.', $count, 'vigilante' ), $count ),
607 + 'message' => $message,
959 608 'count' => $count,
960 609 ) );
961 610 }
962 611
@@ -976,10 +625,11 @@
976 625 if ( 0 === $user_id ) {
977 626 wp_send_json_error( __( 'Invalid user.', 'vigilante' ) );
978 627 }
979 628
980 - // Permission check: own profile or admin
981 - if ( get_current_user_id() !== $user_id && ! current_user_can( 'manage_options' ) ) {
629 + // Permission check: own profile, or a user this one may actually edit.
630 + // manage_options is held by every subsite administrator on a network.
631 + if ( get_current_user_id() !== $user_id && ! current_user_can( 'edit_user', $user_id ) ) {
982 632 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
983 633 }
984 634
985 635 if ( ! class_exists( 'Vigilante_Two_Factor_TOTP' ) ) {
@@ -1194,8 +844,16 @@
1194 844 $results['failed']
1195 845 );
1196 846 }
1197 847
848 + if ( ! empty( $results['skipped'] ) ) {
849 + $message .= ' ' . sprintf(
850 + /* translators: %d: Number of users skipped because the current user cannot edit them */
851 + __( '%d skipped: you cannot edit those users.', 'vigilante' ),
852 + $results['skipped']
853 + );
854 + }
855 +
1198 856 wp_send_json_success( array(
1199 857 'message' => $message,
1200 858 'results' => $results,
1201 859 'resetting_self' => $resetting_self,
@@ -1256,8 +914,16 @@
1256 914 $results['failed']
1257 915 );
1258 916 }
1259 917
918 + if ( ! empty( $results['skipped'] ) ) {
919 + $message .= ' ' . sprintf(
920 + /* translators: %d: Number of users skipped because the current user cannot edit them */
921 + __( '%d skipped: you cannot edit those users.', 'vigilante' ),
922 + $results['skipped']
923 + );
924 + }
925 +
1260 926 wp_send_json_success( array(
1261 927 'message' => $message,
1262 928 'results' => $results,
1263 929 'resetting_self' => $include_self,
@@ -1350,8 +1016,16 @@
1350 1016 $results['failed']
1351 1017 );
1352 1018 }
1353 1019
1020 + if ( ! empty( $results['skipped'] ) ) {
1021 + $message .= ' ' . sprintf(
1022 + /* translators: %d: Number of users skipped because the current user cannot edit them */
1023 + __( '%d skipped: you cannot edit those users.', 'vigilante' ),
1024 + $results['skipped']
1025 + );
1026 + }
1027 +
1354 1028 // Check if current user was included via role membership.
1355 1029 $resetting_self = false;
1356 1030 if ( $include_self ) {
1357 1031 $current_user = wp_get_current_user();
@@ -1380,8 +1054,29 @@
1380 1054 if ( ! $user_id ) {
1381 1055 wp_send_json_error( __( 'Invalid user ID.', 'vigilante' ) );
1382 1056 }
1383 1057
1058 + /*
1059 + * Permission over that account, which on a network only a network
1060 + * administrator has (wp-includes/capabilities.php:75). Same rule the other
1061 + * account tools got in 2.10.3, kept here in 2.11.8.
1062 + *
1063 + * The reason written here until 2.11.10 was that the pending flag is one
1064 + * user meta shared by the whole network, and that stopped being true in
1065 + * this very release: the flag is per site now and approving clears only
1066 + * this site's. The check stays all the same, and deliberately. Approving
1067 + * is what lets somebody into a network whose session cookie is valid on
1068 + * every site of it, and the queue is shown to a site administrator so they
1069 + * can see who is waiting, with the button locked and explained, which is
1070 + * how it has behaved since 2.11.8 and what matriz-red-limpieza-2114.sh
1071 + * checks. Loosening it is a decision about who may let people into a
1072 + * network, not a tidy-up, so it belongs with the rest of the network
1073 + * permissions work in 3.1.0 and not in a security release.
1074 + */
1075 + if ( ! current_user_can( 'edit_user', $user_id ) ) {
1076 + wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1077 + }
1078 +
1384 1079 $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log );
1385 1080 $result = $user_security->approve_user( $user_id, get_current_user_id() );
1386 1081
1387 1082 if ( $result ) {
@@ -1414,8 +1109,14 @@
1414 1109 if ( ! $user_id ) {
1415 1110 wp_send_json_error( __( 'Invalid user ID.', 'vigilante' ) );
1416 1111 }
1417 1112
1113 + // See ajax_approve_user(): the account and its pending flag belong to the
1114 + // whole network (2.11.8).
1115 + if ( ! current_user_can( 'edit_user', $user_id ) ) {
1116 + wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1117 + }
1118 +
1418 1119 $user = get_userdata( $user_id );
1419 1120 $username = $user ? $user->user_login : $user_id;
1420 1121
1421 1122 $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log );
@@ -1454,8 +1155,14 @@
1454 1155 if ( ! $user ) {
1455 1156 wp_send_json_error( __( 'User not found.', 'vigilante' ) );
1456 1157 }
1457 1158
1159 + // Sessions carry IP, User-Agent and login time. manage_options alone is
1160 + // not enough on a network, where it is held per subsite.
1161 + if ( ! current_user_can( 'edit_user', $user_id ) ) {
1162 + wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1163 + }
1164 +
1458 1165 $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log );
1459 1166 $sessions = $user_security->get_user_sessions( $user_id );
1460 1167
1461 1168 wp_send_json_success( array(
@@ -1484,8 +1191,12 @@
1484 1191 if ( ! $user_id || ! $token_hash ) {
1485 1192 wp_send_json_error( __( 'Invalid parameters.', 'vigilante' ) );
1486 1193 }
1487 1194
1195 + if ( ! current_user_can( 'edit_user', $user_id ) ) {
1196 + wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1197 + }
1198 +
1488 1199 $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log );
1489 1200 $result = $user_security->revoke_session( $user_id, $token_hash );
1490 1201
1491 1202 if ( $result ) {
@@ -1513,8 +1224,12 @@
1513 1224 if ( ! $user_id ) {
1514 1225 wp_send_json_error( __( 'Invalid user ID.', 'vigilante' ) );
1515 1226 }
1516 1227
1228 + if ( ! current_user_can( 'edit_user', $user_id ) ) {
1229 + wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1230 + }
1231 +
1517 1232 $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log );
1518 1233 $count = $user_security->revoke_all_sessions( $user_id, $include_current );
1519 1234
1520 1235 wp_send_json_success( array(
@@ -1612,8 +1327,16 @@
1612 1327 if ( ! current_user_can( 'manage_options' ) ) {
1613 1328 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1614 1329 }
1615 1330
1331 + // The dump is taken with $wpdb->prefix, which on the main site of a
1332 + // network matches every subsite table plus the global user tables, and
1333 + // the options it carries include the stored copy of wp-config.php. Same
1334 + // gate the rest of the network-shared operations use.
1335 + if ( ! Vigilante_Settings::can_write_shared_files() ) {
1336 + wp_send_json_error( Vigilante_Settings::get_shared_files_notice() );
1337 + }
1338 +
1616 1339 $backup = new Vigilante_Database_Backup();
1617 1340 $tables = $backup->get_tables();
1618 1341
1619 1342 wp_send_json_success( $tables );
@@ -1630,8 +1353,16 @@
1630 1353 if ( ! current_user_can( 'manage_options' ) ) {
1631 1354 wp_die( esc_html__( 'Permission denied.', 'vigilante' ), 403 );
1632 1355 }
1633 1356
1357 + // The dump is taken with $wpdb->prefix, which on the main site of a
1358 + // network matches every subsite table plus the global user tables, and
1359 + // the options it carries include the stored copy of wp-config.php. Same
1360 + // gate the rest of the network-shared operations use.
1361 + if ( ! Vigilante_Settings::can_write_shared_files() ) {
1362 + wp_die( esc_html( Vigilante_Settings::get_shared_files_notice() ), 403 );
1363 + }
1364 +
1634 1365 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
1635 1366 $tables_raw = isset( $_POST['tables'] ) ? wp_unslash( $_POST['tables'] ) : '';
1636 1367
1637 1368 if ( empty( $tables_raw ) ) {
@@ -1720,8 +1451,14 @@
1720 1451 wp_send_json_error( __( 'Invalid prefix provided.', 'vigilante' ) );
1721 1452 }
1722 1453
1723 1454 $db_prefix = new Vigilante_Database_Prefix();
1455 +
1456 + // On a network the prefix is shared by every site: main site + network admin only
1457 + $allowed = $db_prefix->can_change_prefix();
1458 + if ( is_wp_error( $allowed ) ) {
1459 + wp_send_json_error( $allowed->get_error_message() );
1460 + }
1724 1461
1725 1462 // Validate first
1726 1463 $valid = $db_prefix->validate_prefix( $new_prefix );
1727 1464 if ( is_wp_error( $valid ) ) {