10, 'sslverify' => false, 'headers' => array_merge( Scanner::get_loopback_headers(), array( 'Cookie' => '' ) // Ensure unauthenticated request. ), 'cookies' => array(), ) ); if ( ! \is_wp_error( $response ) ) { $code = \wp_remote_retrieve_response_code( $response ); $body = \wp_remote_retrieve_body( $response ); if ( 200 === $code ) { $data = json_decode( $body, true ); if ( is_array( $data ) && ! empty( $data ) ) { $usernames = array_map( function ( $u ) { return $u['slug'] ?? ''; }, array_slice( $data, 0, 5 ) ); $usernames = array_filter( $usernames ); if ( ! empty( $usernames ) ) { $issues[] = 'rest_api'; $details['rest_api_users'] = $usernames; } } } $details['rest_api_status'] = $code; } else { $details['rest_api_error'] = $response->get_error_message(); } // 2. Check author enumeration via ?author=N (use a real user ID). $test_users = \get_users( array( 'number' => 1, 'orderby' => 'ID', 'order' => 'ASC', 'fields' => 'ID', ) ); $author_id = ! empty( $test_users ) ? (int) $test_users[0] : 1; $author_url = \add_query_arg( 'author', (string) $author_id, $site_url ); $response = \wp_remote_get( $author_url, array( 'timeout' => 10, 'sslverify' => false, 'redirection' => 0, 'headers' => Scanner::get_loopback_headers(), ) ); if ( ! \is_wp_error( $response ) ) { $code = \wp_remote_retrieve_response_code( $response ); $location = \wp_remote_retrieve_header( $response, 'location' ); // A 301 redirect to /author/username/ indicates enumeration is possible. if ( 301 === $code && ! empty( $location ) && preg_match( '/\/author\/([^\/]+)/i', $location, $m ) ) { $issues[] = 'author_archives'; $details['author_archive_user'] = $m[1]; } $details['author_archive_status'] = $code; } if ( empty( $issues ) ) { return static::pass( __( 'User enumeration is properly restricted. Usernames cannot be easily discovered by attackers through the REST API or author archives.', '0-day-analytics' ), $details ); } $messages = array(); if ( in_array( 'rest_api', $issues, true ) ) { $messages[] = sprintf( /* translators: %s: comma-separated list of usernames */ __( 'The REST API users endpoint is publicly accessible and exposes usernames: %s', '0-day-analytics' ), implode( ', ', $details['rest_api_users'] ) ); } if ( in_array( 'author_archives', $issues, true ) ) { $messages[] = sprintf( /* translators: %s: username */ __( 'Author archives reveal usernames via URL redirection (e.g. %s)', '0-day-analytics' ), esc_html( $details['author_archive_user'] ) ); } return static::fail( 'medium', implode( '. ', $messages ) . '. ' . __( 'Knowing valid usernames makes brute-force attacks easier because attackers only need to guess the password.', '0-day-analytics' ), sprintf( /* translators: %s: link to WP Panel Security & API section */ __( 'Disable the REST API users endpoint for non-authenticated users with a security plugin, or add a filter: add_filter(\'rest_endpoints\', function($endpoints){ unset($endpoints[\'/wp/v2/users\']); return $endpoints; }); For author archives, consider using a plugin that disables author enumeration, or add an action: add_action (\'template_redirect\', function () { if (is_author()) { wp_redirect(home_url(), 301); exit; }}); Or use this plugin\'s WP Panel module (Security & API section) to disable author archives, and the REST API module users endpoint to disable it in the REST.', '0-day-analytics' ), \esc_url( \admin_url( 'admin.php?page=advan_wp_panel#security-and-api' ) ), \esc_url( \admin_url( 'admin.php?paged=1&s=users&page=advan_rest_api' ) ) ), $details ); } } }