request( '/xmlrpc.php', array( 'method' => 'GET' ) ); if ( is_wp_error( $response ) ) { return $this->info( __( 'Unable to probe xmlrpc.php (the loopback request failed). Verify manually whether it is reachable.', 'aardwolf-security-scanner' ) ); } $code = (int) wp_remote_retrieve_response_code( $response ); $body = wp_remote_retrieve_body( $response ); // A live XML-RPC endpoint answers a GET with the classic message. $reachable = ( 200 === $code || 405 === $code ) && false !== strpos( $body, 'XML-RPC server accepts POST requests only' ); if ( $reachable ) { return $this->warn( Aardwolf_Check_Result::SEVERITY_MEDIUM, __( 'xmlrpc.php is enabled and responding. Attackers abuse it to amplify brute-force attacks (system.multicall lets them try hundreds of passwords in one request) and to launch pingback-based DDoS reflection.', 'aardwolf-security-scanner' ), __( 'If you do not use the Jetpack/mobile app or remote publishing, disable XML-RPC — many security plugins and hosts offer a toggle, or you can block /xmlrpc.php at the web-server level. If you need it, at minimum disable pingbacks and rate-limit the endpoint.', 'aardwolf-security-scanner' ), 'https://kinsta.com/blog/xmlrpc-php/' ); } return $this->pass( __( 'XML-RPC is disabled or not reachable.', 'aardwolf-security-scanner' ) ); } } /** * readme.html leaks the exact WordPress version. */ class Aardwolf_Check_Readme extends Aardwolf_Check { public function id() { return 'readme_exposed'; } public function label() { return __( 'Version-leaking readme.html is not public', 'aardwolf-security-scanner' ); } public function category() { return __( 'Information Exposure', 'aardwolf-security-scanner' ); } public function run() { $response = $this->request( '/readme.html' ); if ( is_wp_error( $response ) ) { return $this->info( __( 'Unable to probe readme.html. Check manually whether it is accessible.', 'aardwolf-security-scanner' ) ); } $code = (int) wp_remote_retrieve_response_code( $response ); if ( 200 === $code ) { return $this->warn( Aardwolf_Check_Result::SEVERITY_LOW, __( 'The default readme.html is publicly accessible. It advertises your exact WordPress version, letting attackers match your site against version-specific exploits at a glance.', 'aardwolf-security-scanner' ), __( 'Delete /readme.html (and /license.txt) from the site root, or block them at the web server. Note that core updates may recreate readme.html, so add a deny rule rather than relying on deletion alone.', 'aardwolf-security-scanner' ) ); } return $this->pass( __( 'readme.html is not publicly accessible.', 'aardwolf-security-scanner' ) ); } } /** * The generator meta tag advertises the WordPress version. */ class Aardwolf_Check_Version_Disclosure extends Aardwolf_Check { public function id() { return 'version_disclosure'; } public function label() { return __( 'WordPress version is not advertised', 'aardwolf-security-scanner' ); } public function category() { return __( 'Information Exposure', 'aardwolf-security-scanner' ); } public function run() { $response = $this->request( '/' ); if ( is_wp_error( $response ) ) { return $this->info( __( 'Unable to fetch the home page to inspect the generator tag.', 'aardwolf-security-scanner' ) ); } $body = wp_remote_retrieve_body( $response ); $leaks_generator = (bool) preg_match( '/]+name=["\']generator["\'][^>]+WordPress\s*[0-9]/i', $body ); $leaks_query_ver = (bool) preg_match( '/\?ver=[0-9]+\.[0-9]+(\.[0-9]+)?/', $body ) && false !== strpos( $body, 'wp-includes' ); if ( $leaks_generator ) { return $this->warn( Aardwolf_Check_Result::SEVERITY_LOW, __( 'The HTML source includes a tag exposing the WordPress version number. This gives attackers a free fingerprint of exactly which exploits might apply.', 'aardwolf-security-scanner' ), __( 'Remove the generator tag by adding remove_action( \'wp_head\', \'wp_generator\' ); in your theme or a small mu-plugin. Note this is defence-in-depth (security by obscurity), not a substitute for patching.', 'aardwolf-security-scanner' ) ); } if ( $leaks_query_ver ) { return $this->info( __( 'Asset URLs still carry ?ver= query strings, which can hint at versions. This is low risk and mainly a fingerprinting nicety for attackers.', 'aardwolf-security-scanner' ), __( 'Optionally strip version query args from enqueued scripts/styles. Keeping software patched matters far more.', 'aardwolf-security-scanner' ) ); } return $this->pass( __( 'No WordPress version is advertised in the page source.', 'aardwolf-security-scanner' ) ); } } /** * Directory listing is enabled for wp-content/uploads. */ class Aardwolf_Check_Directory_Listing extends Aardwolf_Check { public function id() { return 'directory_listing'; } public function label() { return __( 'Directory browsing is disabled', 'aardwolf-security-scanner' ); } public function category() { return __( 'Information Exposure', 'aardwolf-security-scanner' ); } public function run() { $uploads = wp_get_upload_dir(); $base_url = isset( $uploads['baseurl'] ) ? $uploads['baseurl'] : content_url( '/uploads' ); $response = wp_remote_get( trailingslashit( $base_url ), array( 'timeout' => 10, 'redirection' => 0, 'sslverify' => apply_filters( 'aardwolf_sc_sslverify', true ), ) ); if ( is_wp_error( $response ) ) { return $this->info( __( 'Unable to probe the uploads directory for browsing.', 'aardwolf-security-scanner' ) ); } $code = (int) wp_remote_retrieve_response_code( $response ); $body = wp_remote_retrieve_body( $response ); $listing_visible = 200 === $code && (bool) preg_match( '/