fail( Aardwolf_Check_Result::SEVERITY_MEDIUM, __( 'The Appearance → Theme File Editor and Plugins → Plugin File Editor are enabled. If an attacker steals an administrator session they can write arbitrary PHP straight into your site — turning a compromised login into full code execution.', 'aardwolf-security-scanner' ), __( 'Add define( \'DISALLOW_FILE_EDIT\', true ); to wp-config.php (above the "That\'s all, stop editing!" line). For maximum lockdown you can also use DISALLOW_FILE_MODS to block all in-dashboard installs and updates.', 'aardwolf-security-scanner' ), 'https://wordpress.org/documentation/article/editing-wp-config-php/#disable-the-plugin-and-theme-editor' ); } return $this->pass( __( 'The dashboard file editor is disabled via DISALLOW_FILE_EDIT.', 'aardwolf-security-scanner' ) ); } } /** * WP_DEBUG / debug display is on in production. */ class Aardwolf_Check_Debug_Mode extends Aardwolf_Check { public function id() { return 'debug_mode'; } public function label() { return __( 'Debug output is not exposed', 'aardwolf-security-scanner' ); } public function category() { return __( 'Configuration Hardening', 'aardwolf-security-scanner' ); } public function run() { $debug = defined( 'WP_DEBUG' ) && WP_DEBUG; $debug_display = defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG_DISPLAY; // Displaying errors to visitors leaks paths, plugin internals and more. if ( $debug && ( $debug_display || ! defined( 'WP_DEBUG_DISPLAY' ) ) ) { return $this->fail( Aardwolf_Check_Result::SEVERITY_MEDIUM, __( 'WP_DEBUG is enabled and errors may be printed to the page. Debug output reveals absolute server paths, database details and plugin internals that help an attacker map your environment.', 'aardwolf-security-scanner' ), __( 'On a production site set define( \'WP_DEBUG\', false ); in wp-config.php. If you must keep logging on, set WP_DEBUG_DISPLAY to false and WP_DEBUG_LOG to a path outside the web root so errors go to a file instead of the screen.', 'aardwolf-security-scanner' ), 'https://wordpress.org/documentation/article/debugging-in-wordpress/' ); } if ( $debug ) { return $this->warn( Aardwolf_Check_Result::SEVERITY_LOW, __( 'WP_DEBUG is enabled but display is off. Logging is fine for staging, but debug mode should generally be disabled on production.', 'aardwolf-security-scanner' ), __( 'Confirm WP_DEBUG_LOG points to a file outside the public web root, and disable WP_DEBUG entirely once you have finished troubleshooting.', 'aardwolf-security-scanner' ) ); } return $this->pass( __( 'Debug mode is disabled, so no diagnostic details leak to visitors.', 'aardwolf-security-scanner' ) ); } } /** * Security keys and salts are missing or left at defaults. */ class Aardwolf_Check_Security_Keys extends Aardwolf_Check { public function id() { return 'security_keys'; } public function label() { return __( 'Secret keys and salts are configured', 'aardwolf-security-scanner' ); } public function category() { return __( 'Configuration Hardening', 'aardwolf-security-scanner' ); } public function run() { $keys = array( 'AUTH_KEY', 'SECURE_AUTH_KEY', 'LOGGED_IN_KEY', 'NONCE_KEY', 'AUTH_SALT', 'SECURE_AUTH_SALT', 'LOGGED_IN_SALT', 'NONCE_SALT' ); $missing_or_default = array(); foreach ( $keys as $key ) { if ( ! defined( $key ) ) { $missing_or_default[] = $key; continue; } $value = constant( $key ); // The famous placeholder shipped in the sample config, or something // far too short to be a real salt. if ( empty( $value ) || false !== strpos( (string) $value, 'put your unique phrase here' ) || strlen( (string) $value ) < 32 ) { $missing_or_default[] = $key; } } if ( ! empty( $missing_or_default ) ) { return $this->fail( Aardwolf_Check_Result::SEVERITY_HIGH, sprintf( /* translators: %s: comma separated list of key constants. */ __( 'These authentication keys/salts are missing, empty or left at their placeholder value: %s. Weak keys make session cookies and nonces predictable, undermining login security.', 'aardwolf-security-scanner' ), esc_html( implode( ', ', $missing_or_default ) ) ), __( 'Generate a fresh set at https://api.wordpress.org/secret-key/1.1/salt/ and paste them over the existing definitions in wp-config.php. Changing them logs everyone out, which also boots any attacker holding a stolen session.', 'aardwolf-security-scanner' ), 'https://wordpress.org/documentation/article/editing-wp-config-php/#security-keys' ); } return $this->pass( __( 'All eight authentication keys and salts are defined with strong values.', 'aardwolf-security-scanner' ) ); } } /** * Default "wp_" database table prefix. */ class Aardwolf_Check_Table_Prefix extends Aardwolf_Check { public function id() { return 'table_prefix'; } public function label() { return __( 'Database table prefix is non-default', 'aardwolf-security-scanner' ); } public function category() { return __( 'Configuration Hardening', 'aardwolf-security-scanner' ); } public function run() { global $wpdb; $prefix = ( isset( $wpdb ) && is_object( $wpdb ) ) ? $wpdb->prefix : ''; if ( 'wp_' === $prefix ) { return $this->warn( Aardwolf_Check_Result::SEVERITY_LOW, __( 'The database uses the default "wp_" table prefix. This is not a vulnerability on its own, but a predictable prefix makes blind SQL-injection payloads easier to write.', 'aardwolf-security-scanner' ), __( 'Changing the prefix on a live site is delicate — take a full database backup first. Update $table_prefix in wp-config.php and rename the tables (and fix the options/usermeta key prefixes) with a maintenance plugin or careful SQL. New installs let you set this during setup.', 'aardwolf-security-scanner' ), 'https://wordpress.org/documentation/article/editing-wp-config-php/#table-prefix' ); } return $this->pass( __( 'A custom database table prefix is in use.', 'aardwolf-security-scanner' ) ); } } /** * Admin/login pages are not forced over HTTPS. */ class Aardwolf_Check_Force_Ssl_Admin extends Aardwolf_Check { public function id() { return 'force_ssl_admin'; } public function label() { return __( 'Admin and login are forced over HTTPS', 'aardwolf-security-scanner' ); } public function category() { return __( 'Configuration Hardening', 'aardwolf-security-scanner' ); } public function run() { $forced = defined( 'FORCE_SSL_ADMIN' ) && FORCE_SSL_ADMIN; $site_is_https = 'https' === wp_parse_url( home_url(), PHP_URL_SCHEME ); if ( ! $site_is_https ) { // Covered in more detail by the transport check; note it here too. return $this->warn( Aardwolf_Check_Result::SEVERITY_MEDIUM, __( 'The site is not served over HTTPS, so administrator logins are transmitted in clear text and can be intercepted.', 'aardwolf-security-scanner' ), __( 'Install a TLS certificate (Let\'s Encrypt is free), move the site to https://, then add define( \'FORCE_SSL_ADMIN\', true ); to wp-config.php.', 'aardwolf-security-scanner' ), 'https://wordpress.org/documentation/article/administration-over-ssl/' ); } if ( ! $forced ) { return $this->warn( Aardwolf_Check_Result::SEVERITY_LOW, __( 'HTTPS is available but FORCE_SSL_ADMIN is not set, so the dashboard and login could still be reached over plain HTTP.', 'aardwolf-security-scanner' ), __( 'Add define( \'FORCE_SSL_ADMIN\', true ); to wp-config.php so wp-admin and wp-login.php always use an encrypted connection.', 'aardwolf-security-scanner' ), 'https://wordpress.org/documentation/article/administration-over-ssl/' ); } return $this->pass( __( 'Administrator and login pages are forced over HTTPS.', 'aardwolf-security-scanner' ) ); } }