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
);
}
}
}