| @@ -1545,8 +1545,11 @@ | ||
| 1545 | 1545 | 'reviewChanges' => __( 'Review changes', 'vigilante' ), |
| 1546 | 1546 | 'hideChanges' => __( 'Hide changes', 'vigilante' ), |
| 1547 | 1547 | 'changes' => __( 'Changes', 'vigilante' ), |
| 1548 | 1548 | 'diffUnavailable' => __( 'Diff not available for this file (baseline was created before diff tracking was added). Approve to enable diff on future changes.', 'vigilante' ), |
| 1549 | + 'diffNetwork' => __( 'This file belongs to the whole network, so its line changes are only shown to network administrators, on the main site.', 'vigilante' ), | |
| 1550 | + 'diffRescan' => __( 'Run a new scan to see the line changes of this file.', 'vigilante' ), | |
| 1551 | + 'diffRedaction' => __( 'The line changes of this file are not shown because a value in it could not be hidden safely. The change itself is still detected.', 'vigilante' ), | |
| 1549 | 1552 | 'diffEmpty' => __( 'No line-level changes detected (may be whitespace or reordering).', 'vigilante' ), |
| 1550 | 1553 | 'diffLines' => __( 'lines', 'vigilante' ), |
| 1551 | 1554 | // Under Attack mode strings |
| 1552 | 1555 | 'underAttackConfirmActivate' => __( 'Activate Under Attack mode? All visitors will see a verification page for the next 4 hours.', 'vigilante' ), |
| @@ -2115,10 +2118,46 @@ | ||
| 2115 | 2118 | * |
| 2116 | 2119 | * @since 2.10.4 |
| 2117 | 2120 | * @return bool |
| 2118 | 2121 | */ |
| 2122 | + private function forwarded_chain_readings() { | |
| 2123 | + // Shown, not decided on: the firewall resolves the address elsewhere. | |
| 2124 | + $chain = Vigilante_IP_Utils::trusted_forwarded_for(); | |
| 2125 | + | |
| 2126 | + if ( '' === $chain ) { | |
| 2127 | + return array(); | |
| 2128 | + } | |
| 2129 | + | |
| 2130 | + $public = array(); | |
| 2131 | + | |
| 2132 | + foreach ( explode( ',', $chain ) as $entry ) { | |
| 2133 | + $address = Vigilante_IP_Utils::unmap_ipv4( trim( $entry ) ); | |
| 2134 | + | |
| 2135 | + if ( filter_var( $address, FILTER_VALIDATE_IP ) && ! Vigilante_IP_Utils::is_own_network( $address ) ) { | |
| 2136 | + $public[] = $address; | |
| 2137 | + } | |
| 2138 | + } | |
| 2139 | + | |
| 2140 | + if ( count( $public ) < 2 ) { | |
| 2141 | + return array(); | |
| 2142 | + } | |
| 2143 | + | |
| 2144 | + return array( | |
| 2145 | + 'now' => Vigilante_IP_Utils::client_from_chain( $chain ), | |
| 2146 | + 'before' => $public[0], | |
| 2147 | + ); | |
| 2148 | + } | |
| 2149 | + | |
| 2150 | + /** | |
| 2151 | + * Whether the user tools of this screen are out of reach for this user | |
| 2152 | + * | |
| 2153 | + * @return bool | |
| 2154 | + */ | |
| 2119 | 2155 | private function user_actions_locked() { |
| 2120 | - return is_multisite() && ! current_user_can( 'manage_network_users' ); | |
| 2156 | + // On a single site edit_user maps to edit_users, which a custom role with | |
| 2157 | + // manage_options may lack: since 2.11.8 approving and rejecting a pending | |
| 2158 | + // registration ask for it, so the buttons have to say so there too. | |
| 2159 | + return is_multisite() ? ! current_user_can( 'manage_network_users' ) : ! current_user_can( 'edit_users' ); | |
| 2121 | 2160 | } |
| 2122 | 2161 | |
| 2123 | 2162 | /** |
| 2124 | 2163 | * Print the notice for user tools that cannot be used from this site |
| @@ -2130,9 +2169,13 @@ | ||
| 2130 | 2169 | return; |
| 2131 | 2170 | } |
| 2132 | 2171 | ?> |
| 2133 | 2172 | <div class="notice notice-info inline" style="margin:10px 0 16px;padding:8px 12px;"> |
| 2173 | + <?php if ( is_multisite() ) : ?> | |
| 2134 | 2174 | <p style="margin:0;"><?php esc_html_e( 'These tools act on user accounts, which on a network belong to the whole network rather than to one site. WordPress reserves that to network administrators, so they are managed from the network admin.', 'vigilante' ); ?></p> |
| 2175 | + <?php else : ?> | |
| 2176 | + <p style="margin:0;"><?php esc_html_e( 'These tools act on other user accounts, and your role cannot edit users, so they are not available to you.', 'vigilante' ); ?></p> | |
| 2177 | + <?php endif; ?> | |
| 2135 | 2178 | </div> |
| 2136 | 2179 | <?php |
| 2137 | 2180 | } |
| 2138 | 2181 | |
| @@ -3307,8 +3350,29 @@ | ||
| 3307 | 3350 | </table> |
| 3308 | 3351 | </div> |
| 3309 | 3352 | <?php endif; ?> |
| 3310 | 3353 | |
| 3354 | + <?php | |
| 3355 | + // Since 2.11.8 X-Forwarded-For is read from its end, where the proxy | |
| 3356 | + // writes. The administrator's own request shows whether that end is | |
| 3357 | + // a CDN or a balancer for everybody here. Cross review of 2.11.8. | |
| 3358 | + $xff_readings = $this->forwarded_chain_readings(); | |
| 3359 | + if ( $xff_readings ) : | |
| 3360 | + ?> | |
| 3361 | + <div id="vigilante-xff-chain-notice" class="notice notice-warning inline" style="margin:10px 0 16px;padding:8px 12px;"> | |
| 3362 | + <p style="margin:0;"> | |
| 3363 | + <?php | |
| 3364 | + printf( | |
| 3365 | + /* translators: 1: address Vigilant reads now, 2: address earlier versions read */ | |
| 3366 | + esc_html__( 'Your own request reaches the site with more than one public address in X-Forwarded-For. Vigilant reads the last one, %1$s, which is the one your proxy added; up to version 2.11.7 it read the first one, %2$s, which a visitor can write. If %1$s belongs to a CDN or a load balancer rather than to you, every visitor shares it for rate limiting, login lockouts and the IP lists: choose the header of that CDN in Visitor IP detection, such as CF-Connecting-IP for Cloudflare.', 'vigilante' ), | |
| 3367 | + esc_html( $xff_readings['now'] ), | |
| 3368 | + esc_html( $xff_readings['before'] ) | |
| 3369 | + ); | |
| 3370 | + ?> | |
| 3371 | + </p> | |
| 3372 | + </div> | |
| 3373 | + <?php endif; ?> | |
| 3374 | + | |
| 3311 | 3375 | <h3><?php esc_html_e( 'IP Lists', 'vigilante' ); ?></h3> |
| 3312 | 3376 | <p class="description"> |
| 3313 | 3377 | <?php |
| 3314 | 3378 | printf( |
| @@ -5062,8 +5126,9 @@ | ||
| 5062 | 5126 | <span class="dashicons dashicons-yes-alt"></span> |
| 5063 | 5127 | <p><?php esc_html_e( 'No pending registrations.', 'vigilante' ); ?></p> |
| 5064 | 5128 | </div> |
| 5065 | 5129 | <?php else : ?> |
| 5130 | + <?php $this->render_user_actions_notice(); ?> | |
| 5066 | 5131 | <table class="wp-list-table widefat fixed striped vigilante-pending-users-table"> |
| 5067 | 5132 | <thead> |
| 5068 | 5133 | <tr> |
| 5069 | 5134 | <th><?php esc_html_e( 'User', 'vigilante' ); ?></th> |
| @@ -5092,12 +5157,12 @@ | ||
| 5092 | 5157 | } |
| 5093 | 5158 | ?> |
| 5094 | 5159 | </td> |
| 5095 | 5160 | <td> |
| 5096 | - <button type="button" class="button button-small vigilante-approve-user" data-user-id="<?php echo esc_attr( $pending_user->ID ); ?>"> | |
| 5161 | + <button type="button" class="button button-small vigilante-approve-user" data-user-id="<?php echo esc_attr( $pending_user->ID ); ?>" <?php disabled( $this->user_actions_locked() ); ?>> | |
| 5097 | 5162 | <?php esc_html_e( 'Approve', 'vigilante' ); ?> |
| 5098 | 5163 | </button> |
| 5099 | - <button type="button" class="button button-small vigilante-reject-user" data-user-id="<?php echo esc_attr( $pending_user->ID ); ?>" style="color: #d63638;"> | |
| 5164 | + <button type="button" class="button button-small vigilante-reject-user" data-user-id="<?php echo esc_attr( $pending_user->ID ); ?>" style="color: #d63638;" <?php disabled( $this->user_actions_locked() ); ?>> | |
| 5100 | 5165 | <?php esc_html_e( 'Reject', 'vigilante' ); ?> |
| 5101 | 5166 | </button> |
| 5102 | 5167 | </td> |
| 5103 | 5168 | </tr> |
| @@ -5991,8 +6056,13 @@ | ||
| 5991 | 6056 | */ |
| 5992 | 6057 | private function render_tab_file_integrity() { |
| 5993 | 6058 | $is_disabled = $this->render_module_disabled_notice( 'file_integrity' ); |
| 5994 | 6059 | $options = $this->settings->get_section( 'file_integrity' ); |
| 6060 | + // On the main site of a network the critical-file scan is the network's | |
| 6061 | + // canary for a change to wp-config.php or the root .htaccess, so a | |
| 6062 | + // main-site admin without network rights cannot turn it off. Since | |
| 6063 | + // 2.11.8; see Vigilante_Settings::get_main_site_file_settings(). | |
| 6064 | + $vg_main_locked = $this->main_site_files_locked(); | |
| 5995 | 6065 | $last_scan = get_option( 'vigilante_last_integrity_scan' ); |
| 5996 | 6066 | $last_results = get_option( 'vigilante_last_integrity_results' ); |
| 5997 | 6067 | $ignored_files = get_option( 'vigilante_ignored_files', array() ); |
| 5998 | 6068 | |
| @@ -6118,10 +6188,13 @@ | ||
| 6118 | 6188 | <?php esc_html_e( 'Uploads directory (detect PHP files, double extensions, .htaccess)', 'vigilante' ); ?> |
| 6119 | 6189 | </label> |
| 6120 | 6190 | <br> |
| 6121 | 6191 | <label> |
| 6122 | - <input type="checkbox" name="file_integrity[scan_critical_config]" value="1" <?php checked( $options['scan_critical_config'] ?? true ); ?>> | |
| 6192 | + <input type="checkbox" name="file_integrity[scan_critical_config]" value="1" <?php disabled( $vg_main_locked ); ?> <?php checked( $options['scan_critical_config'] ?? true ); ?>> | |
| 6123 | 6193 | <?php esc_html_e( 'Critical config files (wp-config.php, .htaccess baseline monitoring)', 'vigilante' ); ?> |
| 6194 | + <?php if ( $vg_main_locked ) : ?> | |
| 6195 | + <span class="description" style="display:block;margin-left:24px;"><?php echo esc_html( Vigilante_Settings::get_shared_files_notice() ); ?></span> | |
| 6196 | + <?php endif; ?> | |
| 6124 | 6197 | </label> |
| 6125 | 6198 | <br> |
| 6126 | 6199 | <label> |
| 6127 | 6200 | <input type="checkbox" name="file_integrity[check_closed_plugins]" value="1" <?php checked( $options['check_closed_plugins'] ?? true ); ?>> |
| @@ -6400,9 +6473,15 @@ | ||
| 6400 | 6473 | $crit_diff = $crit_item['diff'] ?? array(); |
| 6401 | 6474 | $crit_id = sanitize_html_class( $crit_file ); |
| 6402 | 6475 | $added_count = is_array( $crit_diff ) ? count( $crit_diff['added'] ?? array() ) : 0; |
| 6403 | 6476 | $removed_count = is_array( $crit_diff ) ? count( $crit_diff['removed'] ?? array() ) : 0; |
| 6404 | - $diff_unavailable = is_array( $crit_diff ) && ! empty( $crit_diff['unavailable'] ); | |
| 6477 | + // The lines of a shared file are for whoever approves it. Results | |
| 6478 | + // stored before 2.11.8 on the main site still carry them, so the | |
| 6479 | + // screen asks too, not only the scan that wrote them. | |
| 6480 | + $diff_network = ( is_array( $crit_diff ) && ! empty( $crit_diff['network'] ) ) || $this->critical_approval_locked(); | |
| 6481 | + $diff_rescan = is_array( $crit_diff ) && ! empty( $crit_diff['rescan'] ); | |
| 6482 | + $diff_redaction = is_array( $crit_diff ) && ! empty( $crit_diff['redaction'] ); | |
| 6483 | + $diff_unavailable = $diff_network || ( is_array( $crit_diff ) && ! empty( $crit_diff['unavailable'] ) ); | |
| 6405 | 6484 | ?> |
| 6406 | 6485 | <tr> |
| 6407 | 6486 | <td><code style="color: #e36210;"><?php echo esc_html( $crit_file ); ?></code></td> |
| 6408 | 6487 | <td> |
| @@ -6439,10 +6518,22 @@ | ||
| 6439 | 6518 | </tr> |
| 6440 | 6519 | <tr id="vigilante-critical-content-<?php echo esc_attr( $crit_id ); ?>" class="vigilante-critical-content-row" style="display:none;"> |
| 6441 | 6520 | <td colspan="3" style="padding: 0;"> |
| 6442 | 6521 | <div class="vigilante-critical-content" style="max-height: 400px; overflow: auto; background: #fff; padding: 10px; font-size: 12px; line-height: 1.5; font-family: Consolas, Monaco, monospace; border-top: 1px solid #c3c4c7;"> |
| 6443 | - <?php if ( $diff_unavailable ) : ?> | |
| 6522 | + <?php if ( $diff_network ) : ?> | |
| 6444 | 6523 | <p style="color: #50575e; font-style: italic; margin: 0;"> |
| 6524 | + <?php esc_html_e( 'This file belongs to the whole network, so its line changes are only shown to network administrators, on the main site.', 'vigilante' ); ?> | |
| 6525 | + </p> | |
| 6526 | + <?php elseif ( $diff_rescan ) : ?> | |
| 6527 | + <p style="color: #50575e; font-style: italic; margin: 0;"> | |
| 6528 | + <?php esc_html_e( 'Run a new scan to see the line changes of this file.', 'vigilante' ); ?> | |
| 6529 | + </p> | |
| 6530 | + <?php elseif ( $diff_redaction ) : ?> | |
| 6531 | + <p style="color: #50575e; font-style: italic; margin: 0;"> | |
| 6532 | + <?php esc_html_e( 'The line changes of this file are not shown because a value in it could not be hidden safely. The change itself is still detected.', 'vigilante' ); ?> | |
| 6533 | + </p> | |
| 6534 | + <?php elseif ( $diff_unavailable ) : ?> | |
| 6535 | + <p style="color: #50575e; font-style: italic; margin: 0;"> | |
| 6445 | 6536 | <?php esc_html_e( 'Diff not available for this file (baseline was created before diff tracking was added). Approve to enable diff on future changes.', 'vigilante' ); ?> |
| 6446 | 6537 | </p> |
| 6447 | 6538 | <?php elseif ( empty( $crit_diff['added'] ) && empty( $crit_diff['removed'] ) ) : ?> |
| 6448 | 6539 | <p style="color: #50575e; font-style: italic; margin: 0;"> |
| @@ -7594,8 +7685,19 @@ | ||
| 7594 | 7685 | // Save new results |
| 7595 | 7686 | update_option( 'vigilante_last_integrity_scan', time() ); |
| 7596 | 7687 | update_option( 'vigilante_last_integrity_results', $results ); |
| 7597 | 7688 | |
| 7689 | + // On the main site the scan does compute the lines of wp-config.php and | |
| 7690 | + // .htaccess, for the network administrator. Somebody without network | |
| 7691 | + // rights gets the change and its sizes, not the lines. | |
| 7692 | + if ( $this->critical_approval_locked() && ! empty( $results['modified'] ) && is_array( $results['modified'] ) ) { | |
| 7693 | + foreach ( $results['modified'] as $index => $item ) { | |
| 7694 | + if ( is_array( $item ) && 'critical_config' === ( $item['type'] ?? '' ) ) { | |
| 7695 | + $results['modified'][ $index ]['diff'] = Vigilante_File_Integrity::network_only_diff(); | |
| 7696 | + } | |
| 7697 | + } | |
| 7698 | + } | |
| 7699 | + | |
| 7598 | 7700 | wp_send_json_success( array( |
| 7599 | 7701 | 'message' => __( 'Scan completed.', 'vigilante' ), |
| 7600 | 7702 | 'results' => $results, |
| 7601 | 7703 | 'ignored_count' => count( get_option( 'vigilante_ignored_files', array() ) ), |
| @@ -7629,11 +7731,41 @@ | ||
| 7629 | 7731 | if ( ! current_user_can( 'manage_options' ) ) { |
| 7630 | 7732 | wp_send_json_error( __( 'Permission denied.', 'vigilante' ) ); |
| 7631 | 7733 | } |
| 7632 | 7734 | |
| 7735 | + $results = get_option( 'vigilante_last_integrity_results' ); | |
| 7736 | + $scanned_at = get_option( 'vigilante_last_integrity_scan' ); | |
| 7737 | + | |
| 7633 | 7738 | delete_option( 'vigilante_last_integrity_results' ); |
| 7634 | 7739 | delete_option( 'vigilante_last_integrity_scan' ); |
| 7635 | 7740 | |
| 7741 | + /* | |
| 7742 | + * A pending change to wp-config.php or the root .htaccess is closed by | |
| 7743 | + * approving it, which takes the network. Clearing the results was one | |
| 7744 | + * more way to close it without, until the next scan: the ignore list was | |
| 7745 | + * shut in 2.11.8 and this button was left open, found by the cross | |
| 7746 | + * review of 2.11.8. So for somebody who cannot approve, those entries | |
| 7747 | + * stay and everything else goes. | |
| 7748 | + */ | |
| 7749 | + if ( $this->critical_approval_locked() && is_array( $results ) && ! empty( $results['modified'] ) && is_array( $results['modified'] ) ) { | |
| 7750 | + $critical = array_values( | |
| 7751 | + array_filter( | |
| 7752 | + $results['modified'], | |
| 7753 | + function ( $item ) { | |
| 7754 | + return is_array( $item ) && 'critical_config' === ( $item['type'] ?? '' ); | |
| 7755 | + } | |
| 7756 | + ) | |
| 7757 | + ); | |
| 7758 | + | |
| 7759 | + if ( $critical ) { | |
| 7760 | + $results['modified'] = $critical; | |
| 7761 | + $results['suspicious'] = array(); | |
| 7762 | + $results['extra'] = array(); | |
| 7763 | + update_option( 'vigilante_last_integrity_results', $results ); | |
| 7764 | + update_option( 'vigilante_last_integrity_scan', $scanned_at ? $scanned_at : time() ); | |
| 7765 | + } | |
| 7766 | + } | |
| 7767 | + | |
| 7636 | 7768 | if ( $this->database ) { |
| 7637 | 7769 | $this->database->clear_file_hashes(); |
| 7638 | 7770 | } |
| 7639 | 7771 | |
| @@ -7659,8 +7791,14 @@ | ||
| 7659 | 7791 | if ( empty( $file ) ) { |
| 7660 | 7792 | wp_send_json_error( __( 'No file specified.', 'vigilante' ) ); |
| 7661 | 7793 | } |
| 7662 | 7794 | |
| 7795 | + // A change to a shared file is closed by approving it, and approving it | |
| 7796 | + // takes the network. Ignoring it would close the same warning without. | |
| 7797 | + if ( $this->critical_approval_locked() && in_array( $file, array( 'wp-config.php', '.htaccess' ), true ) ) { | |
| 7798 | + wp_send_json_error( $this->critical_approval_notice() ); | |
| 7799 | + } | |
| 7800 | + | |
| 7663 | 7801 | $file_integrity = new Vigilante_File_Integrity( $this->settings, $this->database ); |
| 7664 | 7802 | $file_integrity->ignore_file( $file ); |
| 7665 | 7803 | |
| 7666 | 7804 | // Also remove the file from stored scan results so UI updates |
| @@ -7724,12 +7862,14 @@ | ||
| 7724 | 7862 | if ( ! is_array( $raw_files ) ) { |
| 7725 | 7863 | wp_send_json_error( __( 'Invalid request.', 'vigilante' ) ); |
| 7726 | 7864 | } |
| 7727 | 7865 | |
| 7728 | - $files = array(); | |
| 7866 | + $files = array(); | |
| 7867 | + $shared = $this->critical_approval_locked() ? array( 'wp-config.php', '.htaccess' ) : array(); | |
| 7729 | 7868 | foreach ( $raw_files as $f ) { |
| 7730 | 7869 | $clean = sanitize_text_field( $f ); |
| 7731 | - if ( '' !== $clean ) { | |
| 7870 | + // Same rule as ajax_ignore_file() for the two shared files. | |
| 7871 | + if ( '' !== $clean && ! in_array( $clean, $shared, true ) ) { | |
| 7732 | 7872 | $files[] = $clean; |
| 7733 | 7873 | } |
| 7734 | 7874 | } |
| 7735 | 7875 | |