fail( $is_admin_role ? Aardwolf_Check_Result::SEVERITY_MEDIUM : Aardwolf_Check_Result::SEVERITY_LOW, __( 'An account with the username "admin" exists. This is the first username brute-force tools try, so half of the credentials are already known to the attacker.', 'aardwolf-security-scanner' ), __( 'Create a new administrator account with a unique username, log in as that user, then delete the "admin" account (assigning its content to the new user). WordPress does not let you rename a user, so this replace-and-delete approach is the supported route.', 'aardwolf-security-scanner' ), 'https://wordpress.org/documentation/article/users-add-new-screen/' ); } return $this->pass( __( 'No account uses the predictable "admin" username.', 'aardwolf-security-scanner' ) ); } } /** * The login name is exposed as the author slug (user enumeration). */ class Aardwolf_Check_User_Enumeration extends Aardwolf_Check { public function id() { return 'user_enumeration'; } public function label() { return __( 'Usernames are not publicly enumerable', 'aardwolf-security-scanner' ); } public function category() { return __( 'Accounts & Authentication', 'aardwolf-security-scanner' ); } public function run() { // We test the two vectors an attacker would actually use, over live // HTTP, rather than inspecting stored user data. This means that when // the "Harden" option blocks these vectors the check correctly flips to // a pass on the next scan. $detail = array(); // 1. REST API users endpoint: is it publicly readable without auth? $response = $this->request( '/wp-json/wp/v2/users' ); if ( ! is_wp_error( $response ) ) { $code = (int) wp_remote_retrieve_response_code( $response ); $body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( 200 === $code && is_array( $body ) && ! empty( $body ) && isset( $body[0]['slug'] ) ) { $detail[] = __( 'the REST API /wp/v2/users endpoint lists users to anonymous visitors', 'aardwolf-security-scanner' ); } } // 2. Author scan: does /?author=1 redirect to a public /author/{slug}/ // archive that leaks the login name? WordPress does this by default; the // hardening layer (and most security plugins) intercept it. $response = $this->request( '/?author=1' ); if ( ! is_wp_error( $response ) ) { $code = (int) wp_remote_retrieve_response_code( $response ); $location = wp_remote_retrieve_header( $response, 'location' ); if ( in_array( $code, array( 301, 302 ), true ) && $location && false !== stripos( $location, '/author/' ) ) { $detail[] = __( 'the ?author= parameter redirects to a public author archive that reveals the login name', 'aardwolf-security-scanner' ); } } if ( ! empty( $detail ) ) { return $this->fail( Aardwolf_Check_Result::SEVERITY_MEDIUM, sprintf( /* translators: %s: list of exposure vectors. */ __( 'Valid usernames can be harvested because %s. Attackers use these lists to seed credential-stuffing and brute-force attacks.', 'aardwolf-security-scanner' ), implode( __( ' and ', 'aardwolf-security-scanner' ), $detail ) ), __( 'Restrict the REST users endpoint and block the ?author= redirect — most security plugins offer a one-click toggle, or you can filter rest_endpoints and add a redirect rule / web-server rewrite. As defence in depth, also set each user\'s "Nickname" and public display name to something different from their login.', 'aardwolf-security-scanner' ), 'https://developer.wordpress.org/rest-api/frequently-asked-questions/#require-authentication-for-all-requests' ); } return $this->pass( __( 'Usernames are not trivially exposed through author archives or the REST API.', 'aardwolf-security-scanner' ) ); } } /** * Open user registration with an over-privileged default role. */ class Aardwolf_Check_Registration extends Aardwolf_Check { public function id() { return 'open_registration'; } public function label() { return __( 'Registration default role is safe', 'aardwolf-security-scanner' ); } public function category() { return __( 'Accounts & Authentication', 'aardwolf-security-scanner' ); } public function run() { $registration_open = (bool) get_option( 'users_can_register' ); $default_role = get_option( 'default_role', 'subscriber' ); $privileged_roles = array( 'administrator', 'editor', 'author' ); if ( $registration_open && in_array( $default_role, $privileged_roles, true ) ) { return $this->fail( Aardwolf_Check_Result::SEVERITY_HIGH, sprintf( /* translators: %s: the configured default role. */ __( 'Anyone can self-register and every new account is created as "%s", which can publish or modify content. This lets an attacker gain a foothold without any exploit.', 'aardwolf-security-scanner' ), esc_html( $default_role ) ), __( 'Under Settings → General set the default role to "Subscriber", or disable "Anyone can register" entirely if you do not need public sign-ups.', 'aardwolf-security-scanner' ), 'https://wordpress.org/documentation/article/settings-general-screen/' ); } if ( $registration_open ) { return $this->warn( Aardwolf_Check_Result::SEVERITY_LOW, __( 'Public registration is enabled (new users are Subscribers). This is usually fine, but it is an extra surface for spam and account abuse.', 'aardwolf-security-scanner' ), __( 'If you do not intentionally offer public accounts, turn off "Anyone can register" under Settings → General. If you do need it, add CAPTCHA/anti-spam protection to the registration form.', 'aardwolf-security-scanner' ) ); } return $this->pass( __( 'Public registration is disabled, or new users receive the low-privilege Subscriber role.', 'aardwolf-security-scanner' ) ); } }