Building Your Own WP Security Scanner – Complete Guide
Table of Contents
Security is paramount for WordPress websites. While commercial security scanners exist, building your own custom security scanner gives you complete control, deep insights into WordPress vulnerabilities, and the ability to tailor checks specifically to your infrastructure. This comprehensive guide will walk you through creating a professional-grade WordPress security scanner from scratch.
1. Security Scanner Overview
A comprehensive WordPress security scanner should identify vulnerabilities across multiple layers of your installation. Let’s examine the key components and threat categories:
🔍 Core Scanning
Validates WordPress core files against official checksums and detects unauthorized modifications.
🔌 Plugin Analysis
Identifies outdated, vulnerable, or abandoned plugins with known security issues.
🎨 Theme Auditing
Scans theme files for malicious code, backdoors, and security vulnerabilities.
🗄️ Database Security
Checks for SQL injection vectors, weak table prefixes, and unauthorized user accounts.
📁 File Permissions
Audits file and directory permissions to prevent unauthorized access.
🔐 Configuration Review
Analyzes wp-config.php and server configuration for security misconfigurations.
| Vulnerability Type | Severity Level | Detection Method | Common Impact |
|---|---|---|---|
| Outdated Core | Critical | Version comparison | Site takeover, data breach |
| Vulnerable Plugins | Critical | CVE database check | Remote code execution |
| Weak Passwords | High | Password strength analysis | Unauthorized access |
| File Modifications | High | Checksum validation | Malware injection |
| Debug Mode Enabled | Medium | Config file parsing | Information disclosure |
| Directory Listing | Medium | HTTP response check | File enumeration |
| Default Admin Username | Low | Database query | Brute force vulnerability |
2. Scanner Architecture
Our security scanner will follow a modular architecture with clear separation of concerns. This approach ensures maintainability, extensibility, and adherence to WordPress coding standards.
- Scanner Core: Main orchestration class managing scan execution
- Check Modules: Individual vulnerability detection classes
- Report Generator: Formats and outputs scan results
- Database Handler: Manages scan history and results storage
- API Integration: Communicates with vulnerability databases
- Notification System: Alerts administrators of critical findings
2.1 Class Structure Diagram
The scanner follows object-oriented principles with the following class hierarchy:
<?php
/**
* Main Security Scanner Class
*
* @package WP_Security_Scanner
* @since 1.0.0
*/
namespace WP_Security_Scanner;
/**
* Class Security_Scanner
*
* Core scanner orchestration class following WPCS standards.
*/
class Security_Scanner {
/**
* Scanner version.
*
* @var string
*/
const VERSION = '1.0.0';
/**
* Check modules registry.
*
* @var array
*/
private $check_modules = array();
/**
* Scan results.
*
* @var array
*/
private $results = array();
/**
* Scanner configuration.
*
* @var array
*/
private $config = array();
/**
* Constructor.
*
* @param array $config Scanner configuration options.
*/
public function __construct( array $config = array() ) {
$this->config = wp_parse_args(
$config,
$this->get_default_config()
);
$this->register_check_modules();
}
/**
* Get default scanner configuration.
*
* @return array Default configuration values.
*/
private function get_default_config() {
return array(
'enable_core_checks' => true,
'enable_plugin_checks' => true,
'enable_theme_checks' => true,
'enable_db_checks' => true,
'enable_file_checks' => true,
'scan_timeout' => 300,
'max_memory_limit' => '512M',
'enable_notifications' => true,
'notification_threshold' => 'high',
);
}
/**
* Register all security check modules.
*
* @return void
*/
private function register_check_modules() {
$modules = array(
'core' => new Checks\Core_Integrity_Check(),
'plugins' => new Checks\Plugin_Vulnerability_Check(),
'themes' => new Checks\Theme_Security_Check(),
'database' => new Checks\Database_Security_Check(),
'files' => new Checks\File_Permission_Check(),
'config' => new Checks\Configuration_Check(),
'users' => new Checks\User_Security_Check(),
'malware' => new Checks\Malware_Signature_Check(),
);
/**
* Filter the registered security check modules.
*
* @param array $modules Array of check module instances.
*/
$this->check_modules = apply_filters(
'wp_security_scanner_modules',
$modules
);
}
/**
* Execute security scan.
*
* @return array Scan results.
*/
public function run_scan() {
// Set execution limits.
$this->set_execution_limits();
// Clear previous results.
$this->results = array(
'scan_id' => $this->generate_scan_id(),
'timestamp' => current_time( 'mysql' ),
'version' => self::VERSION,
'site_url' => get_site_url(),
'checks' => array(),
'summary' => array(),
'total_issues' => 0,
);
// Execute each check module.
foreach ( $this->check_modules as $module_name => $module ) {
if ( $this->should_run_module( $module_name ) ) {
$this->results['checks'][ $module_name ] = $module->execute();
}
}
// Generate summary.
$this->generate_summary();
// Store results.
$this->store_results();
// Send notifications if needed.
$this->send_notifications();
return $this->results;
}
/**
* Set execution limits for scan.
*
* @return void
*/
private function set_execution_limits() {
// Prevent timeout during scan.
if ( ! ini_get( 'safe_mode' ) ) {
set_time_limit( $this->config['scan_timeout'] );
}
// Increase memory limit if needed.
$current_limit = wp_convert_hr_to_bytes( WP_MEMORY_LIMIT );
$required_limit = wp_convert_hr_to_bytes( $this->config['max_memory_limit'] );
if ( $current_limit < $required_limit ) {
ini_set( 'memory_limit', $this->config['max_memory_limit'] );
}
}
/**
* Check if module should run based on configuration.
*
* @param string $module_name Module identifier.
* @return bool Whether module should execute.
*/
private function should_run_module( $module_name ) {
$config_key = 'enable_' . $module_name . '_checks';
return isset( $this->config[ $config_key ] )
? (bool) $this->config[ $config_key ]
: true;
}
/**
* Generate unique scan identifier.
*
* @return string Unique scan ID.
*/
private function generate_scan_id() {
return wp_generate_password( 32, false, false );
}
/**
* Generate scan summary from individual check results.
*
* @return void
*/
private function generate_summary() {
$severity_counts = array(
'critical' => 0,
'high' => 0,
'medium' => 0,
'low' => 0,
'info' => 0,
);
foreach ( $this->results['checks'] as $check_results ) {
if ( isset( $check_results['issues'] ) ) {
foreach ( $check_results['issues'] as $issue ) {
$severity = strtolower( $issue['severity'] );
if ( isset( $severity_counts[ $severity ] ) ) {
$severity_counts[ $severity ]++;
$this->results['total_issues']++;
}
}
}
}
$this->results['summary'] = $severity_counts;
}
/**
* Store scan results in database.
*
* @return bool Whether storage was successful.
*/
private function store_results() {
global $wpdb;
$table_name = $wpdb->prefix . 'security_scan_results';
$inserted = $wpdb->insert(
$table_name,
array(
'scan_id' => $this->results['scan_id'],
'timestamp' => $this->results['timestamp'],
'results_data' => wp_json_encode( $this->results ),
'total_issues' => $this->results['total_issues'],
),
array( '%s', '%s', '%s', '%d' )
);
return false !== $inserted;
}
/**
* Send notifications if critical issues detected.
*
* @return void
*/
private function send_notifications() {
if ( ! $this->config['enable_notifications'] ) {
return;
}
$threshold = $this->config['notification_threshold'];
$should_notify = false;
switch ( $threshold ) {
case 'critical':
$should_notify = $this->results['summary']['critical'] > 0;
break;
case 'high':
$should_notify = $this->results['summary']['critical'] > 0
|| $this->results['summary']['high'] > 0;
break;
case 'medium':
$should_notify = $this->results['total_issues'] > 0;
break;
}
if ( $should_notify ) {
do_action(
'wp_security_scanner_send_notification',
$this->results
);
}
}
/**
* Get scan results.
*
* @return array Current scan results.
*/
public function get_results() {
return $this->results;
}
}3. Building the Core Structure
The base check class provides a common interface for all security modules. Each specific check extends this abstract class:
<?php
/**
* Abstract Security Check Class
*
* @package WP_Security_Scanner\Checks
* @since 1.0.0
*/
namespace WP_Security_Scanner\Checks;
/**
* Class Abstract_Security_Check
*
* Base class for all security check modules.
*/
abstract class Abstract_Security_Check {
/**
* Check identifier.
*
* @var string
*/
protected $check_id;
/**
* Check name.
*
* @var string
*/
protected $check_name;
/**
* Check description.
*
* @var string
*/
protected $check_description;
/**
* Check results.
*
* @var array
*/
protected $results = array();
/**
* Execute the security check.
*
* Must be implemented by child classes.
*
* @return array Check results.
*/
abstract public function execute();
/**
* Add an issue to results.
*
* @param string $severity Issue severity (critical|high|medium|low|info).
* @param string $title Issue title.
* @param string $description Issue description.
* @param array $metadata Additional metadata.
* @return void
*/
protected function add_issue( $severity, $title, $description, array $metadata = array() ) {
$this->results['issues'][] = array(
'severity' => $severity,
'title' => sanitize_text_field( $title ),
'description' => wp_kses_post( $description ),
'metadata' => $metadata,
'timestamp' => current_time( 'mysql' ),
);
}
/**
* Get check results.
*
* @return array Results array.
*/
public function get_results() {
return $this->results;
}
/**
* Initialize results array.
*
* @return void
*/
protected function init_results() {
$this->results = array(
'check_id' => $this->check_id,
'check_name' => $this->check_name,
'check_description' => $this->check_description,
'status' => 'pending',
'issues' => array(),
'passed' => 0,
'failed' => 0,
);
}
/**
* Mark check as complete.
*
* @param string $status Status (passed|failed|error).
* @return void
*/
protected function complete_check( $status = 'passed' ) {
$this->results['status'] = $status;
$this->results['completed_at'] = current_time( 'mysql' );
$this->results['failed'] = count( $this->results['issues'] );
}
/**
* Sanitize file path for output.
*
* @param string $path File path.
* @return string Sanitized path.
*/
protected function sanitize_file_path( $path ) {
$path = str_replace( ABSPATH, '', $path );
return sanitize_text_field( $path );
}
}4. Implementing Vulnerability Checks
4.1 Core Integrity Verification
Checking WordPress core file integrity ensures no unauthorized modifications have been made. This is crucial for detecting backdoors and malware:
<?php
/**
* WordPress Core Integrity Check
*
* @package WP_Security_Scanner\Checks
* @since 1.0.0
*/
namespace WP_Security_Scanner\Checks;
/**
* Class Core_Integrity_Check
*
* Validates WordPress core files against official checksums.
*/
class Core_Integrity_Check extends Abstract_Security_Check {
/**
* Constructor.
*/
public function __construct() {
$this->check_id = 'core_integrity';
$this->check_name = __( 'WordPress Core Integrity', 'wp-security-scanner' );
$this->check_description = __(
'Verifies WordPress core files against official checksums',
'wp-security-scanner'
);
}
/**
* Execute core integrity check.
*
* @return array Check results.
*/
public function execute() {
$this->init_results();
// Get current WordPress version.
global $wp_version;
// Fetch official checksums from WordPress.org API.
$checksums = $this->fetch_core_checksums( $wp_version );
if ( is_wp_error( $checksums ) ) {
$this->add_issue(
'high',
__( 'Unable to Verify Core Files', 'wp-security-scanner' ),
sprintf(
/* translators: %s: Error message */
__( 'Could not fetch checksums from WordPress.org: %s', 'wp-security-scanner' ),
$checksums->get_error_message()
),
array( 'error_code' => $checksums->get_error_code() )
);
$this->complete_check( 'error' );
return $this->results;
}
// Verify each core file.
$modified_files = array();
$missing_files = array();
foreach ( $checksums as $file => $expected_checksum ) {
$file_path = ABSPATH . $file;
if ( ! file_exists( $file_path ) ) {
$missing_files[] = $file;
continue;
}
$actual_checksum = md5_file( $file_path );
if ( $actual_checksum !== $expected_checksum ) {
$modified_files[] = array(
'file' => $file,
'expected_checksum' => $expected_checksum,
'actual_checksum' => $actual_checksum,
);
}
}
// Report modified files.
if ( ! empty( $modified_files ) ) {
foreach ( $modified_files as $file_data ) {
$this->add_issue(
'critical',
__( 'Core File Modified', 'wp-security-scanner' ),
sprintf(
/* translators: %s: File path */
__( 'WordPress core file has been modified: %s', 'wp-security-scanner' ),
'<code>' . esc_html( $file_data['file'] ) . '</code>'
),
$file_data
);
}
}
// Report missing files.
if ( ! empty( $missing_files ) ) {
foreach ( $missing_files as $file ) {
$this->add_issue(
'high',
__( 'Core File Missing', 'wp-security-scanner' ),
sprintf(
/* translators: %s: File path */
__( 'WordPress core file is missing: %s', 'wp-security-scanner' ),
'<code>' . esc_html( $file ) . '</code>'
),
array( 'file' => $file )
);
}
}
$this->complete_check(
empty( $modified_files ) && empty( $missing_files ) ? 'passed' : 'failed'
);
return $this->results;
}
/**
* Fetch core checksums from WordPress.org API.
*
* @param string $version WordPress version.
* @param string $locale WordPress locale.
* @return array|WP_Error Checksums array or error object.
*/
private function fetch_core_checksums( $version, $locale = 'en_US' ) {
$api_url = sprintf(
'https://api.wordpress.org/core/checksums/1.0/?version=%s&locale=%s',
$version,
$locale
);
$response = wp_remote_get(
$api_url,
array(
'timeout' => 30,
'sslverify' => true,
)
);
if ( is_wp_error( $response ) ) {
return $response;
}
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
if ( ! isset( $data['checksums'] ) ) {
return new \WP_Error(
'invalid_response',
__( 'Invalid response from WordPress.org API', 'wp-security-scanner' )
);
}
return $data['checksums'];
}
}4.2 Plugin Vulnerability Detection
Detecting vulnerable plugins is essential for WordPress security. This module integrates with the WPScan vulnerability database:
To use the WPScan Vulnerability Database API, you’ll need to register for a free API key at wpscan.com. The free tier allows 25 requests per day.
<?php
/**
* Plugin Vulnerability Check
*
* @package WP_Security_Scanner\Checks
* @since 1.0.0
*/
namespace WP_Security_Scanner\Checks;
/**
* Class Plugin_Vulnerability_Check
*
* Scans installed plugins for known vulnerabilities.
*/
class Plugin_Vulnerability_Check extends Abstract_Security_Check {
/**
* WPScan API endpoint.
*
* @var string
*/
const WPSCAN_API_URL = 'https://wpscan.com/api/v3/plugins/';
/**
* API token for WPScan.
*
* @var string
*/
private $api_token;
/**
* Constructor.
*/
public function __construct() {
$this->check_id = 'plugin_vulnerabilities';
$this->check_name = __( 'Plugin Vulnerability Check', 'wp-security-scanner' );
$this->check_description = __(
'Scans installed plugins for known security vulnerabilities',
'wp-security-scanner'
);
// Get API token from options.
$this->api_token = get_option( 'wp_security_scanner_wpscan_token', '' );
}
/**
* Execute plugin vulnerability check.
*
* @return array Check results.
*/
public function execute() {
$this->init_results();
if ( empty( $this->api_token ) ) {
$this->add_issue(
'info',
__( 'WPScan API Token Not Configured', 'wp-security-scanner' ),
__(
'Configure a WPScan API token to enable vulnerability scanning.',
'wp-security-scanner'
)
);
$this->complete_check( 'error' );
return $this->results;
}
// Get all installed plugins.
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$all_plugins = get_plugins();
foreach ( $all_plugins as $plugin_file => $plugin_data ) {
$plugin_slug = dirname( $plugin_file );
// Skip if plugin slug is empty (single-file plugins).
if ( empty( $plugin_slug ) || '.' === $plugin_slug ) {
$plugin_slug = basename( $plugin_file, '.php' );
}
$this->check_plugin_vulnerabilities(
$plugin_slug,
$plugin_data['Name'],
$plugin_data['Version']
);
}
$this->complete_check(
empty( $this->results['issues'] ) ? 'passed' : 'failed'
);
return $this->results;
}
/**
* Check specific plugin for vulnerabilities.
*
* @param string $plugin_slug Plugin slug.
* @param string $plugin_name Plugin name.
* @param string $plugin_version Plugin version.
* @return void
*/
private function check_plugin_vulnerabilities( $plugin_slug, $plugin_name, $plugin_version ) {
$vulnerabilities = $this->fetch_plugin_vulnerabilities( $plugin_slug );
if ( is_wp_error( $vulnerabilities ) ) {
return;
}
if ( empty( $vulnerabilities ) ) {
return;
}
// Check if current version is affected.
foreach ( $vulnerabilities as $vuln ) {
if ( $this->is_version_vulnerable( $plugin_version, $vuln ) ) {
$this->add_issue(
$this->map_vulnerability_severity( $vuln ),
sprintf(
/* translators: %s: Plugin name */
__( 'Vulnerable Plugin: %s', 'wp-security-scanner' ),
$plugin_name
),
$this->format_vulnerability_description( $vuln, $plugin_version ),
array(
'plugin_slug' => $plugin_slug,
'plugin_version' => $plugin_version,
'vuln_id' => $vuln['id'],
'cvss_score' => $vuln['cvss']['score'] ?? null,
)
);
}
}
}
/**
* Fetch plugin vulnerabilities from WPScan API.
*
* @param string $plugin_slug Plugin slug.
* @return array|WP_Error Vulnerabilities array or error.
*/
private function fetch_plugin_vulnerabilities( $plugin_slug ) {
$api_url = self::WPSCAN_API_URL . $plugin_slug;
$response = wp_remote_get(
$api_url,
array(
'timeout' => 15,
'headers' => array(
'Authorization' => 'Token token=' . $this->api_token,
),
)
);
if ( is_wp_error( $response ) ) {
return $response;
}
$response_code = wp_remote_retrieve_response_code( $response );
// 404 means no vulnerabilities found.
if ( 404 === $response_code ) {
return array();
}
if ( 200 !== $response_code ) {
return new \WP_Error(
'api_error',
sprintf(
/* translators: %d: HTTP response code */
__( 'WPScan API returned error code: %d', 'wp-security-scanner' ),
$response_code
)
);
}
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
return $data[ $plugin_slug ]['vulnerabilities'] ?? array();
}
/**
* Check if version is vulnerable based on vulnerability data.
*
* @param string $version Version to check.
* @param array $vuln Vulnerability data.
* @return bool Whether version is vulnerable.
*/
private function is_version_vulnerable( $version, $vuln ) {
$fixed_in = $vuln['fixed_in'] ?? null;
if ( null === $fixed_in ) {
// No fix available, all versions vulnerable.
return true;
}
// Version is vulnerable if it's lower than the fixed version.
return version_compare( $version, $fixed_in, '<' );
}
/**
* Map WPScan CVSS score to severity level.
*
* @param array $vuln Vulnerability data.
* @return string Severity level.
*/
private function map_vulnerability_severity( $vuln ) {
$cvss_score = $vuln['cvss']['score'] ?? 0;
if ( $cvss_score >= 9.0 ) {
return 'critical';
} elseif ( $cvss_score >= 7.0 ) {
return 'high';
} elseif ( $cvss_score >= 4.0 ) {
return 'medium';
} else {
return 'low';
}
}
/**
* Format vulnerability description.
*
* @param array $vuln Vulnerability data.
* @param string $version Current plugin version.
* @return string Formatted description.
*/
private function format_vulnerability_description( $vuln, $version ) {
$description = sprintf(
/* translators: 1: Vulnerability title, 2: Current version */
__( '<strong>%1$s</strong>
Current version: %2$s', 'wp-security-scanner' ),
esc_html( $vuln['title'] ),
esc_html( $version )
);
if ( isset( $vuln['fixed_in'] ) ) {
$description .= sprintf(
/* translators: %s: Fixed version */
__( '
Fixed in version: %s', 'wp-security-scanner' ),
esc_html( $vuln['fixed_in'] )
);
} else {
$description .= '
' . __(
'<strong>No fix available yet</strong>',
'wp-security-scanner'
);
}
if ( isset( $vuln['cvss']['score'] ) ) {
$description .= sprintf(
/* translators: %s: CVSS score */
__( '
CVSS Score: %s', 'wp-security-scanner' ),
esc_html( $vuln['cvss']['score'] )
);
}
return $description;
}
}5. File Integrity Monitoring
File integrity monitoring detects unauthorized changes to critical files and identifies potential malware. This implementation uses file hashing and pattern matching:
<?php
/**
* File Permission and Integrity Check
*
* @package WP_Security_Scanner\Checks
* @since 1.0.0
*/
namespace WP_Security_Scanner\Checks;
/**
* Class File_Permission_Check
*
* Audits file and directory permissions.
*/
class File_Permission_Check extends Abstract_Security_Check {
/**
* Recommended file permissions.
*
* @var array
*/
private $recommended_permissions = array(
'files' => 0644,
'directories' => 0755,
);
/**
* Critical files to check.
*
* @var array
*/
private $critical_files = array(
'wp-config.php',
'.htaccess',
'index.php',
);
/**
* Constructor.
*/
public function __construct() {
$this->check_id = 'file_permissions';
$this->check_name = __( 'File Permission Check', 'wp-security-scanner' );
$this->check_description = __(
'Audits file and directory permissions for security issues',
'wp-security-scanner'
);
}
/**
* Execute file permission check.
*
* @return array Check results.
*/
public function execute() {
$this->init_results();
// Check wp-config.php permissions.
$this->check_wp_config_permissions();
// Check .htaccess permissions.
$this->check_htaccess_permissions();
// Check uploads directory permissions.
$this->check_uploads_permissions();
// Check plugin/theme directories.
$this->check_content_directories();
// Scan for suspicious files.
$this->scan_suspicious_files();
$this->complete_check(
empty( $this->results['issues'] ) ? 'passed' : 'failed'
);
return $this->results;
}
/**
* Check wp-config.php permissions.
*
* @return void
*/
private function check_wp_config_permissions() {
$config_file = ABSPATH . 'wp-config.php';
if ( ! file_exists( $config_file ) ) {
$this->add_issue(
'critical',
__( 'wp-config.php Not Found', 'wp-security-scanner' ),
__(
'The wp-config.php file could not be located.',
'wp-security-scanner'
)
);
return;
}
$perms = fileperms( $config_file ) & 0777;
// wp-config.php should be 0600 or 0640 (owner read/write only).
if ( $perms > 0640 ) {
$this->add_issue(
'high',
__( 'wp-config.php Has Insecure Permissions', 'wp-security-scanner' ),
sprintf(
/* translators: 1: Current permissions, 2: Recommended permissions */
__(
'Current permissions: %1$s. Recommended: %2$s or more restrictive.',
'wp-security-scanner'
),
$this->format_permissions( $perms ),
$this->format_permissions( 0640 )
),
array(
'file' => 'wp-config.php',
'current_permissions' => $perms,
'recommended_permissions' => 0640,
)
);
}
// Check if wp-config.php is publicly accessible.
if ( $this->is_file_publicly_accessible( 'wp-config.php' ) ) {
$this->add_issue(
'critical',
__( 'wp-config.php Publicly Accessible', 'wp-security-scanner' ),
__(
'The wp-config.php file can be accessed directly via HTTP. This is a critical security risk.',
'wp-security-scanner'
),
array( 'file' => 'wp-config.php' )
);
}
}
/**
* Check .htaccess permissions.
*
* @return void
*/
private function check_htaccess_permissions() {
$htaccess_file = ABSPATH . '.htaccess';
if ( ! file_exists( $htaccess_file ) ) {
return; // .htaccess is optional.
}
$perms = fileperms( $htaccess_file ) & 0777;
// .htaccess should be 0644 (owner write, all read).
if ( $perms > 0644 ) {
$this->add_issue(
'medium',
__( '.htaccess Has Insecure Permissions', 'wp-security-scanner' ),
sprintf(
/* translators: 1: Current permissions, 2: Recommended permissions */
__(
'Current permissions: %1$s. Recommended: %2$s.',
'wp-security-scanner'
),
$this->format_permissions( $perms ),
$this->format_permissions( 0644 )
),
array(
'file' => '.htaccess',
'current_permissions' => $perms,
'recommended_permissions' => 0644,
)
);
}
}
/**
* Check uploads directory permissions.
*
* @return void
*/
private function check_uploads_permissions() {
$upload_dir = wp_upload_dir();
$uploads_path = $upload_dir['basedir'];
if ( ! is_dir( $uploads_path ) ) {
return;
}
// Check for PHP files in uploads directory.
$php_files = $this->find_php_files_in_uploads( $uploads_path );
if ( ! empty( $php_files ) ) {
foreach ( $php_files as $php_file ) {
$this->add_issue(
'critical',
__( 'PHP File in Uploads Directory', 'wp-security-scanner' ),
sprintf(
/* translators: %s: File path */
__(
'Potentially malicious PHP file found: %s',
'wp-security-scanner'
),
'<code>' . esc_html( $this->sanitize_file_path( $php_file ) ) . '</code>'
),
array( 'file' => $php_file )
);
}
}
}
/**
* Find PHP files in uploads directory.
*
* @param string $directory Directory to scan.
* @param int $depth Current recursion depth.
* @return array Found PHP files.
*/
private function find_php_files_in_uploads( $directory, $depth = 0 ) {
$php_files = array();
$max_depth = 5; // Limit recursion depth.
if ( $depth > $max_depth ) {
return $php_files;
}
$iterator = new \DirectoryIterator( $directory );
foreach ( $iterator as $file ) {
if ( $file->isDot() ) {
continue;
}
if ( $file->isDir() ) {
$php_files = array_merge(
$php_files,
$this->find_php_files_in_uploads(
$file->getPathname(),
$depth + 1
)
);
} elseif ( 'php' === $file->getExtension() ) {
$php_files[] = $file->getPathname();
}
}
return $php_files;
}
/**
* Check content directories permissions.
*
* @return void
*/
private function check_content_directories() {
$content_dirs = array(
WP_CONTENT_DIR . '/plugins',
WP_CONTENT_DIR . '/themes',
);
foreach ( $content_dirs as $dir ) {
if ( ! is_dir( $dir ) ) {
continue;
}
$perms = fileperms( $dir ) & 0777;
// Directories should be 0755 maximum.
if ( $perms > 0755 ) {
$this->add_issue(
'medium',
__( 'Directory Has Insecure Permissions', 'wp-security-scanner' ),
sprintf(
/* translators: 1: Directory path, 2: Current permissions, 3: Recommended permissions */
__(
'Directory %1$s has permissions %2$s. Recommended: %3$s.',
'wp-security-scanner'
),
'<code>' . esc_html( $this->sanitize_file_path( $dir ) ) . '</code>',
$this->format_permissions( $perms ),
$this->format_permissions( 0755 )
),
array(
'directory' => $dir,
'current_permissions' => $perms,
'recommended_permissions' => 0755,
)
);
}
}
}
/**
* Scan for suspicious files.
*
* @return void
*/
private function scan_suspicious_files() {
$suspicious_patterns = array(
'*.suspected',
'*.bak.php',
'*~',
'*.swp',
);
$root_dir = ABSPATH;
$suspicious_files = array();
foreach ( $suspicious_patterns as $pattern ) {
$found_files = glob( $root_dir . $pattern );
if ( ! empty( $found_files ) ) {
$suspicious_files = array_merge( $suspicious_files, $found_files );
}
}
foreach ( $suspicious_files as $file ) {
$this->add_issue(
'medium',
__( 'Suspicious File Detected', 'wp-security-scanner' ),
sprintf(
/* translators: %s: File path */
__(
'Potentially suspicious file found: %s',
'wp-security-scanner'
),
'<code>' . esc_html( $this->sanitize_file_path( $file ) ) . '</code>'
),
array( 'file' => $file )
);
}
}
/**
* Check if file is publicly accessible.
*
* @param string $file Relative file path.
* @return bool Whether file is accessible.
*/
private function is_file_publicly_accessible( $file ) {
$url = site_url( $file );
$response = wp_remote_head(
$url,
array(
'timeout' => 5,
'redirection' => 0,
'sslverify' => false,
)
);
if ( is_wp_error( $response ) ) {
return false;
}
$response_code = wp_remote_retrieve_response_code( $response );
// 200 means file is accessible.
return 200 === $response_code;
}
/**
* Format permissions as octal string.
*
* @param int $perms Permissions value.
* @return string Formatted permissions.
*/
private function format_permissions( $perms ) {
return sprintf( '0%o', $perms );
}
}6. Database Security Scanning
Database security checks identify SQL injection vulnerabilities, weak configurations, and suspicious user accounts:
$wpdb class methods when interacting with the WordPress database. Never concatenate user input directly into SQL queries.<?php
/**
* Database Security Check
*
* @package WP_Security_Scanner\Checks
* @since 1.0.0
*/
namespace WP_Security_Scanner\Checks;
/**
* Class Database_Security_Check
*
* Audits database security configuration and content.
*/
class Database_Security_Check extends Abstract_Security_Check {
/**
* Constructor.
*/
public function __construct() {
$this->check_id = 'database_security';
$this->check_name = __( 'Database Security Check', 'wp-security-scanner' );
$this->check_description = __(
'Analyzes database configuration and identifies security issues',
'wp-security-scanner'
);
}
/**
* Execute database security check.
*
* @return array Check results.
*/
public function execute() {
global $wpdb;
$this->init_results();
// Check table prefix.
$this->check_table_prefix();
// Check for default admin username.
$this->check_default_admin_username();
// Check for users with weak passwords.
$this->check_weak_passwords();
// Check for suspicious user accounts.
$this->check_suspicious_users();
// Check for admin users without email.
$this->check_users_without_email();
// Check database version.
$this->check_database_version();
$this->complete_check(
empty( $this->results['issues'] ) ? 'passed' : 'failed'
);
return $this->results;
}
/**
* Check database table prefix.
*
* @return void
*/
private function check_table_prefix() {
global $wpdb;
// Default WordPress prefix is 'wp_'.
if ( 'wp_' === $wpdb->prefix ) {
$this->add_issue(
'low',
__( 'Default Database Table Prefix', 'wp-security-scanner' ),
__(
'Using the default table prefix "wp_" makes your database more vulnerable to SQL injection attacks. Consider changing it to a unique prefix.',
'wp-security-scanner'
),
array( 'current_prefix' => $wpdb->prefix )
);
}
}
/**
* Check for default admin username.
*
* @return void
*/
private function check_default_admin_username() {
global $wpdb;
// Check if user with username 'admin' exists.
$admin_user = $wpdb->get_var(
$wpdb->prepare(
"SELECT ID FROM {$wpdb->users} WHERE user_login = %s",
'admin'
)
);
if ( $admin_user ) {
$this->add_issue(
'medium',
__( 'Default Admin Username Detected', 'wp-security-scanner' ),
__(
'A user account with the username "admin" exists. This is a common target for brute force attacks. Consider renaming this account.',
'wp-security-scanner'
),
array( 'user_id' => $admin_user )
);
}
}
/**
* Check for users with weak passwords.
*
* Note: WordPress stores hashed passwords, so we can only check
* for common weak password patterns in user metadata.
*
* @return void
*/
private function check_weak_passwords() {
global $wpdb;
// Get all administrator users.
$admin_users = get_users(
array(
'role' => 'administrator',
'fields' => array( 'ID', 'user_login', 'user_email' ),
)
);
foreach ( $admin_users as $user ) {
// Check if user has set up 2FA (example meta key).
$has_2fa = get_user_meta( $user->ID, 'two_factor_enabled', true );
if ( ! $has_2fa ) {
$this->add_issue(
'medium',
__( 'Administrator Without Two-Factor Authentication', 'wp-security-scanner' ),
sprintf(
/* translators: %s: Username */
__(
'Administrator account "%s" does not have two-factor authentication enabled.',
'wp-security-scanner'
),
esc_html( $user->user_login )
),
array(
'user_id' => $user->ID,
'user_login' => $user->user_login,
)
);
}
}
}
/**
* Check for suspicious user accounts.
*
* @return void
*/
private function check_suspicious_users() {
global $wpdb;
// Check for users created in the last 7 days with admin role.
$recent_admins = $wpdb->get_results(
$wpdb->prepare(
"SELECT u.ID, u.user_login, u.user_email, u.user_registered
FROM {$wpdb->users} u
INNER JOIN {$wpdb->usermeta} um ON u.ID = um.user_id
WHERE um.meta_key = %s
AND um.meta_value LIKE %s
AND u.user_registered > DATE_SUB(NOW(), INTERVAL 7 DAY)
ORDER BY u.user_registered DESC",
$wpdb->prefix . 'capabilities',
'%administrator%'
)
);
if ( ! empty( $recent_admins ) ) {
foreach ( $recent_admins as $user ) {
$this->add_issue(
'high',
__( 'Recently Created Administrator Account', 'wp-security-scanner' ),
sprintf(
/* translators: 1: Username, 2: Registration date */
__(
'Administrator account "%1$s" was created recently on %2$s. Verify this is a legitimate account.',
'wp-security-scanner'
),
esc_html( $user->user_login ),
esc_html( $user->user_registered )
),
array(
'user_id' => $user->ID,
'user_login' => $user->user_login,
'user_registered' => $user->user_registered,
)
);
}
}
// Check for users with suspicious login patterns.
$suspicious_logins = array( 'admin', 'test', 'demo', 'guest', 'root' );
foreach ( $suspicious_logins as $login ) {
$user = get_user_by( 'login', $login );
if ( $user && user_can( $user, 'administrator' ) ) {
$this->add_issue(
'medium',
__( 'Suspicious Administrator Username', 'wp-security-scanner' ),
sprintf(
/* translators: %s: Username */
__(
'Administrator account with suspicious username detected: "%s"',
'wp-security-scanner'
),
esc_html( $login )
),
array(
'user_id' => $user->ID,
'user_login' => $login,
)
);
}
}
}
/**
* Check for admin users without email addresses.
*
* @return void
*/
private function check_users_without_email() {
global $wpdb;
$users_without_email = $wpdb->get_results(
$wpdb->prepare(
"SELECT u.ID, u.user_login
FROM {$wpdb->users} u
INNER JOIN {$wpdb->usermeta} um ON u.ID = um.user_id
WHERE um.meta_key = %s
AND um.meta_value LIKE %s
AND (u.user_email = '' OR u.user_email IS NULL)",
$wpdb->prefix . 'capabilities',
'%administrator%'
)
);
if ( ! empty( $users_without_email ) ) {
foreach ( $users_without_email as $user ) {
$this->add_issue(
'medium',
__( 'Administrator Without Email Address', 'wp-security-scanner' ),
sprintf(
/* translators: %s: Username */
__(
'Administrator account "%s" does not have an email address configured.',
'wp-security-scanner'
),
esc_html( $user->user_login )
),
array(
'user_id' => $user->ID,
'user_login' => $user->user_login,
)
);
}
}
}
/**
* Check database version.
*
* @return void
*/
private function check_database_version() {
global $wpdb;
$db_version = $wpdb->db_version();
// MySQL versions below 5.6 or MariaDB below 10.0 are EOL.
if ( version_compare( $db_version, '5.6', '<' ) ) {
$this->add_issue(
'high',
__( 'Outdated Database Version', 'wp-security-scanner' ),
sprintf(
/* translators: %s: Database version */
__(
'Your database version (%s) is outdated and no longer receives security updates. Upgrade to a supported version.',
'wp-security-scanner'
),
esc_html( $db_version )
),
array( 'db_version' => $db_version )
);
}
}
}7. Reporting and Alerting
A comprehensive reporting system presents scan results in an actionable format. This includes HTML reports, email notifications, and dashboard widgets:
<?php
/**
* Security Scanner Report Generator
*
* @package WP_Security_Scanner\Reports
* @since 1.0.0
*/
namespace WP_Security_Scanner\Reports;
/**
* Class Report_Generator
*
* Generates security scan reports in various formats.
*/
class Report_Generator {
/**
* Scan results data.
*
* @var array
*/
private $results;
/**
* Constructor.
*
* @param array $results Scan results.
*/
public function __construct( array $results ) {
$this->results = $results;
}
/**
* Generate HTML report.
*
* @return string HTML report.
*/
public function generate_html_report() {
ob_start();
?>
<div class="wp-security-report">
<div class="report-header">
<h2><?php esc_html_e( 'Security Scan Report', 'wp-security-scanner' ); ?></h2>
<?php
printf(
/* translators: 1: Scan date, 2: Site URL */
esc_html__( 'Scan completed: %1$s | Site: %2$s', 'wp-security-scanner' ),
esc_html( $this->results['timestamp'] ),
esc_html( $this->results['site_url'] )
);
?>
</div>
<div class="report-summary">
<h3><?php esc_html_e( 'Summary', 'wp-security-scanner' ); ?></h3>
<div class="summary-grid">
<?php $this->render_summary_card( 'critical', __( 'Critical', 'wp-security-scanner' ) ); ?>
<?php $this->render_summary_card( 'high', __( 'High', 'wp-security-scanner' ) ); ?>
<?php $this->render_summary_card( 'medium', __( 'Medium', 'wp-security-scanner' ) ); ?>
<?php $this->render_summary_card( 'low', __( 'Low', 'wp-security-scanner' ) ); ?>
</div>
</div>
<div class="report-details">
<h3><?php esc_html_e( 'Detailed Findings', 'wp-security-scanner' ); ?></h3>
<?php $this->render_detailed_findings(); ?>
</div>
</div>
<?php
return ob_get_clean();
}
/**
* Render summary card.
*
* @param string $severity Severity level.
* @param string $label Display label.
* @return void
*/
private function render_summary_card( $severity, $label ) {
$count = $this->results['summary'][ $severity ] ?? 0;
$class = 'summary-card severity-' . $severity;
?>
<div class="<?php echo esc_attr( $class ); ?>">
<div class="card-count"><?php echo esc_html( $count ); ?></div>
<div class="card-label"><?php echo esc_html( $label ); ?></div>
</div>
<?php
}
/**
* Render detailed findings.
*
* @return void
*/
private function render_detailed_findings() {
foreach ( $this->results['checks'] as $check_name => $check_data ) {
if ( empty( $check_data['issues'] ) ) {
continue;
}
?>
<div class="check-section">
<h4><?php echo esc_html( $check_data['check_name'] ); ?></h4>
<?php echo esc_html( $check_data['check_description'] ); ?>
<table class="issues-table">
<thead>
<tr>
<th><?php esc_html_e( 'Severity', 'wp-security-scanner' ); ?></th>
<th><?php esc_html_e( 'Issue', 'wp-security-scanner' ); ?></th>
<th><?php esc_html_e( 'Description', 'wp-security-scanner' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $check_data['issues'] as $issue ) : ?>
<tr class="severity-<?php echo esc_attr( strtolower( $issue['severity'] ) ); ?>">
<td>
<span class="severity-badge">
<?php echo esc_html( ucfirst( $issue['severity'] ) ); ?>
</span>
</td>
<td><?php echo esc_html( $issue['title'] ); ?></td>
<td><?php echo wp_kses_post( $issue['description'] ); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php
}
}
/**
* Generate email notification.
*
* @return bool Whether email was sent successfully.
*/
public function send_email_notification() {
$admin_email = get_option( 'admin_email' );
$site_name = get_bloginfo( 'name' );
$subject = sprintf(
/* translators: 1: Site name, 2: Number of issues */
__( '[%1$s] Security Scan Alert - %2$d Issues Found', 'wp-security-scanner' ),
$site_name,
$this->results['total_issues']
);
$message = $this->generate_email_body();
$headers = array(
'Content-Type: text/html; charset=UTF-8',
);
return wp_mail( $admin_email, $subject, $message, $headers );
}
/**
* Generate email body.
*
* @return string Email HTML body.
*/
private function generate_email_body() {
ob_start();
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: #0073aa; color: white; padding: 20px; text-align: center; }
.summary { background: #f5f5f5; padding: 15px; margin: 20px 0; }
.severity-critical { color: #c62828; font-weight: bold; }
.severity-high { color: #e65100; font-weight: bold; }
.severity-medium { color: #f57f17; }
.severity-low { color: #2e7d32; }
table { width: 100%; border-collapse: collapse; margin: 20px 0; }
th, td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; }
th { background: #f5f5f5; font-weight: bold; }
.footer { margin-top: 30px; padding-top: 20px; border-top: 2px solid #ddd; font-size: 0.9em; color: #666; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1><?php esc_html_e( 'Security Scan Alert', 'wp-security-scanner' ); ?></h1>
</div>
<div class="summary">
<h2><?php esc_html_e( 'Summary', 'wp-security-scanner' ); ?></h2>
<?php
printf(
/* translators: 1: Total issues, 2: Site URL */
esc_html__( '%1$d security issues found on %2$s', 'wp-security-scanner' ),
esc_html( $this->results['total_issues'] ),
esc_html( $this->results['site_url'] )
);
?>
<ul>
<li class="severity-critical">
<?php
printf(
/* translators: %d: Number of critical issues */
esc_html__( 'Critical: %d', 'wp-security-scanner' ),
esc_html( $this->results['summary']['critical'] )
);
?>
</li>
<li class="severity-high">
<?php
printf(
/* translators: %d: Number of high issues */
esc_html__( 'High: %d', 'wp-security-scanner' ),
esc_html( $this->results['summary']['high'] )
);
?>
</li>
<li class="severity-medium">
<?php
printf(
/* translators: %d: Number of medium issues */
esc_html__( 'Medium: %d', 'wp-security-scanner' ),
esc_html( $this->results['summary']['medium'] )
);
?>
</li>
<li class="severity-low">
<?php
printf(
/* translators: %d: Number of low issues */
esc_html__( 'Low: %d', 'wp-security-scanner' ),
esc_html( $this->results['summary']['low'] )
);
?>
</li>
</ul>
</div>
<h2><?php esc_html_e( 'Action Required', 'wp-security-scanner' ); ?></h2>
<?php esc_html_e( 'Please log in to your WordPress admin panel to review the full security report and take appropriate action.', 'wp-security-scanner' ); ?>
<div class="footer">
<?php
printf(
/* translators: %s: Current date and time */
esc_html__( 'Scan completed: %s', 'wp-security-scanner' ),
esc_html( current_time( 'mysql' ) )
);
?>
<?php esc_html_e( 'This is an automated security notification from WP Security Scanner.', 'wp-security-scanner' ); ?>
</div>
</div>
</body>
</html>
<?php
return ob_get_clean();
}
/**
* Generate JSON export.
*
* @return string JSON-encoded results.
*/
public function generate_json_export() {
return wp_json_encode( $this->results, JSON_PRETTY_PRINT );
}
/**
* Generate CSV export.
*
* @return string CSV data.
*/
public function generate_csv_export() {
$csv_data = array();
// CSV header.
$csv_data[] = array(
'Check Name',
'Severity',
'Issue Title',
'Description',
'Timestamp',
);
// Add issues.
foreach ( $this->results['checks'] as $check_data ) {
if ( empty( $check_data['issues'] ) ) {
continue;
}
foreach ( $check_data['issues'] as $issue ) {
$csv_data[] = array(
$check_data['check_name'],
$issue['severity'],
$issue['title'],
wp_strip_all_tags( $issue['description'] ),
$issue['timestamp'],
);
}
}
// Convert to CSV string.
ob_start();
$output = fopen( 'php://output', 'w' );
foreach ( $csv_data as $row ) {
fputcsv( $output, $row );
}
fclose( $output );
return ob_get_clean();
}
}8. Automation and Scheduling
Automate security scans using WordPress cron to ensure continuous monitoring:
<?php
/**
* Security Scanner Automation
*
* @package WP_Security_Scanner\Automation
* @since 1.0.0
*/
namespace WP_Security_Scanner\Automation;
use WP_Security_Scanner\Security_Scanner;
use WP_Security_Scanner\Reports\Report_Generator;
/**
* Class Scanner_Scheduler
*
* Manages automated security scan scheduling.
*/
class Scanner_Scheduler {
/**
* Cron hook name.
*
* @var string
*/
const CRON_HOOK = 'wp_security_scanner_scheduled_scan';
/**
* Initialize scheduler.
*
* @return void
*/
public function init() {
add_action( self::CRON_HOOK, array( $this, 'run_scheduled_scan' ) );
add_action( 'wp_security_scanner_send_notification', array( $this, 'handle_notification' ) );
}
/**
* Schedule recurring scans.
*
* @param string $frequency Cron frequency (hourly|twicedaily|daily|weekly).
* @return bool Whether scheduling was successful.
*/
public function schedule_scans( $frequency = 'daily' ) {
// Unschedule existing scans.
$this->unschedule_scans();
// Schedule new scans.
$timestamp = wp_next_scheduled( self::CRON_HOOK );
if ( false === $timestamp ) {
return wp_schedule_event(
time(),
$frequency,
self::CRON_HOOK
);
}
return true;
}
/**
* Unschedule all scans.
*
* @return bool Whether unscheduling was successful.
*/
public function unschedule_scans() {
$timestamp = wp_next_scheduled( self::CRON_HOOK );
if ( false !== $timestamp ) {
return wp_unschedule_event( $timestamp, self::CRON_HOOK );
}
return true;
}
/**
* Run scheduled security scan.
*
* @return void
*/
public function run_scheduled_scan() {
// Prevent concurrent scans.
if ( get_transient( 'wp_security_scanner_running' ) ) {
return;
}
set_transient( 'wp_security_scanner_running', true, 600 );
try {
// Initialize scanner.
$scanner = new Security_Scanner(
array(
'enable_core_checks' => true,
'enable_plugin_checks' => true,
'enable_theme_checks' => true,
'enable_db_checks' => true,
'enable_file_checks' => true,
'enable_notifications' => true,
'notification_threshold' => 'high',
)
);
// Execute scan.
$results = $scanner->run_scan();
// Log scan completion.
error_log(
sprintf(
'WP Security Scanner: Scheduled scan completed. Found %d issues.',
$results['total_issues']
)
);
} catch ( \Exception $e ) {
error_log(
sprintf(
'WP Security Scanner: Scheduled scan failed - %s',
$e->getMessage()
)
);
} finally {
delete_transient( 'wp_security_scanner_running' );
}
}
/**
* Handle security notification.
*
* @param array $results Scan results.
* @return void
*/
public function handle_notification( $results ) {
$report_generator = new Report_Generator( $results );
$report_generator->send_email_notification();
}
/**
* Get next scheduled scan time.
*
* @return int|false Timestamp or false if not scheduled.
*/
public function get_next_scan_time() {
return wp_next_scheduled( self::CRON_HOOK );
}
/**
* Register custom cron schedules.
*
* @param array $schedules Existing schedules.
* @return array Modified schedules.
*/
public function add_cron_schedules( $schedules ) {
$schedules['weekly'] = array(
'interval' => 604800,
'display' => __( 'Once Weekly', 'wp-security-scanner' ),
);
return $schedules;
}
}
// Initialize scheduler.
add_filter( 'cron_schedules', array( new Scanner_Scheduler(), 'add_cron_schedules' ) );9. Best Practices and Security Considerations
- Sanitize All Inputs: Use WordPress sanitization functions for all user inputs
- Escape All Outputs: Use
esc_html(),esc_attr(), andesc_url()appropriately - Use Nonces: Verify intent with WordPress nonces for all actions
- Check Capabilities: Always verify user permissions before executing sensitive operations
- Prepare SQL Queries: Use
$wpdb->prepare()for all database queries - Limit API Calls: Implement rate limiting and caching for external API requests
- Validate File Operations: Use
wp_check_filetype()and validate paths - Log Security Events: Maintain audit trails of security-related actions
9.1 Implementation Checklist
| Category | Implementation | Priority | Complexity |
|---|---|---|---|
| Core Integrity | Checksum validation against official WordPress API | Critical | Medium |
| Plugin Scanning | WPScan API integration for vulnerability detection | Critical | Medium |
| File Permissions | Recursive directory scanning with permission validation | High | Low |
| Database Security | User enumeration, table prefix, and configuration checks | High | Low |
| Malware Detection | Signature-based scanning with pattern matching | High | High |
| Automated Scanning | WordPress Cron integration with configurable frequency | Medium | Low |
| Report Generation | HTML, JSON, and CSV export capabilities | Medium | Medium |
| Email Notifications | Automated alerts with configurable thresholds | Low | Low |
9.2 Performance Optimization
- Implement scan result caching to avoid redundant checks
- Use transients to store API responses temporarily
- Batch database queries where possible
- Limit file system recursion depth to prevent memory exhaustion
- Run intensive scans during off-peak hours
- Consider background processing for large sites
9.3 Extending the Scanner
The modular architecture makes it easy to add custom security checks. Here’s an example of creating a custom check module:
<?php
/**
* Custom Security Check Example
*
* @package WP_Security_Scanner\Checks
* @since 1.0.0
*/
namespace WP_Security_Scanner\Checks;
/**
* Class Configuration_Check
*
* Checks WordPress configuration for security issues.
*/
class Configuration_Check extends Abstract_Security_Check {
/**
* Constructor.
*/
public function __construct() {
$this->check_id = 'configuration';
$this->check_name = __( 'Configuration Security Check', 'wp-security-scanner' );
$this->check_description = __(
'Analyzes wp-config.php and WordPress configuration',
'wp-security-scanner'
);
}
/**
* Execute configuration check.
*
* @return array Check results.
*/
public function execute() {
$this->init_results();
// Check debug mode.
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
$this->add_issue(
'medium',
__( 'Debug Mode Enabled', 'wp-security-scanner' ),
__(
'WP_DEBUG is enabled. This should be disabled in production environments to prevent information disclosure.',
'wp-security-scanner'
)
);
}
// Check file editing.
if ( ! defined( 'DISALLOW_FILE_EDIT' ) || ! DISALLOW_FILE_EDIT ) {
$this->add_issue(
'high',
__( 'File Editing Not Disabled', 'wp-security-scanner' ),
__(
'WordPress file editor is enabled. Set DISALLOW_FILE_EDIT to true in wp-config.php to prevent unauthorized code changes.',
'wp-security-scanner'
)
);
}
// Check SSL.
if ( ! is_ssl() ) {
$this->add_issue(
'high',
__( 'SSL Not Configured', 'wp-security-scanner' ),
__(
'Your site is not using HTTPS. Enable SSL to encrypt data transmission and protect user credentials.',
'wp-security-scanner'
)
);
}
// Check security keys and salts.
$this->check_security_keys();
// Check file upload settings.
$this->check_file_upload_settings();
$this->complete_check(
empty( $this->results['issues'] ) ? 'passed' : 'failed'
);
return $this->results;
}
/**
* Check WordPress security keys and salts.
*
* @return void
*/
private function check_security_keys() {
$keys = array(
'AUTH_KEY',
'SECURE_AUTH_KEY',
'LOGGED_IN_KEY',
'NONCE_KEY',
'AUTH_SALT',
'SECURE_AUTH_SALT',
'LOGGED_IN_SALT',
'NONCE_SALT',
);
$default_key = 'put your unique phrase here';
foreach ( $keys as $key ) {
if ( ! defined( $key ) ) {
$this->add_issue(
'critical',
sprintf(
/* translators: %s: Key name */
__( 'Security Key Not Defined: %s', 'wp-security-scanner' ),
$key
),
__(
'Security keys and salts should be defined in wp-config.php. Generate new keys at https://api.wordpress.org/secret-key/1.1/salt/',
'wp-security-scanner'
),
array( 'key' => $key )
);
continue;
}
$key_value = constant( $key );
// Check if using default value.
if ( false !== strpos( $key_value, $default_key ) ) {
$this->add_issue(
'critical',
sprintf(
/* translators: %s: Key name */
__( 'Default Security Key: %s', 'wp-security-scanner' ),
$key
),
__(
'Security key is using default value. Generate unique keys at https://api.wordpress.org/secret-key/1.1/salt/',
'wp-security-scanner'
),
array( 'key' => $key )
);
}
// Check key length (should be reasonably long).
if ( strlen( $key_value ) < 64 ) {
$this->add_issue(
'high',
sprintf(
/* translators: %s: Key name */
__( 'Weak Security Key: %s', 'wp-security-scanner' ),
$key
),
__(
'Security key is too short. Use a longer, more complex key for better security.',
'wp-security-scanner'
),
array( 'key' => $key )
);
}
}
}
/**
* Check file upload settings.
*
* @return void
*/
private function check_file_upload_settings() {
$allowed_file_types = get_allowed_mime_types();
// Check if dangerous file types are allowed.
$dangerous_types = array(
'exe' => 'application/x-msdownload',
'php' => 'application/x-php',
'phtml' => 'application/x-php',
'sh' => 'application/x-sh',
);
foreach ( $dangerous_types as $ext => $mime ) {
if ( isset( $allowed_file_types[ $ext ] ) ) {
$this->add_issue(
'high',
__( 'Dangerous File Type Allowed', 'wp-security-scanner' ),
sprintf(
/* translators: %s: File extension */
__(
'File type "%s" is allowed for upload. This could allow execution of malicious code.',
'wp-security-scanner'
),
esc_html( $ext )
),
array(
'extension' => $ext,
'mime_type' => $mime,
)
);
}
}
}
}Conclusion
Building your own WordPress security scanner provides deep insights into your site’s security posture and allows you to customize checks for your specific needs. This implementation follows WordPress coding standards, uses best security practices, and provides a solid foundation that can be extended with additional checks.
Remember to keep your scanner updated, regularly review new vulnerability patterns, and contribute back to the WordPress security community by responsibly disclosing any vulnerabilities you discover.
Comments (0)
Join the conversation. to leave a comment.