allowed_ips_file = ITK_PATH . 'config/allowed-ips.conf'; $this->load_allowed_ips(); add_action('init', [$this, 'protect_wp_login'], 0); add_action('init', [$this, 'block_sensitive_files'], 0); add_action('init', [$this, 'block_malicious_queries'], 0); add_action('init', [$this, 'block_author_scans'], 0); add_action('init', [$this, 'custom_login_url'], 0); add_action('send_headers', [$this, 'add_security_headers']); add_action('wp_loaded', [$this, 'wp_loaded_custom_login']); add_filter('the_generator', '__return_empty_string'); add_filter('wp_redirect', [$this, 'redirect_filter'], 10, 2); add_filter('network_site_url', [$this, 'network_url_filter'], 10, 3); add_filter('site_url', [$this, 'site_url_filter'], 10, 4); add_filter('wp_handle_upload_prefilter', [$this, 'filter_uploaded_files']); } /* ── wp-login protection ──────────────────────────────────── */ public function protect_wp_login(): void { if (ITK_Whitelist::allowed()) return; $options = get_option('itk_security', []); if (empty($options['protect_wp_login'])) return; $uri = $_SERVER['REQUEST_URI'] ?? ''; if (strpos($uri, 'wp-login.php') === false) return; $ip = $this->get_client_ip(); $is_allowed = false; foreach ($this->allowed_ips as $allowed) { if ($ip === $allowed) { $is_allowed = true; break; } if (strpos($allowed, '/') !== false && $this->ip_in_cidr($ip, $allowed)) { $is_allowed = true; break; } } if (!$is_allowed) { $this->send_403($options, 'Access to login page not allowed from your IP address.'); } $ua = $_SERVER['HTTP_USER_AGENT'] ?? ''; $protocol = $_SERVER['SERVER_PROTOCOL'] ?? ''; if (empty($ua) || $protocol === 'HTTP/1.0') { $this->send_403($options, 'Invalid request detected.'); } } /* ── Security headers ─────────────────────────────────────── */ public function add_security_headers(): void { $options = get_option('itk_security', []); if (empty($options['add_security_headers'])) return; if (headers_sent()) return; header_remove('X-Powered-By'); header('X-Content-Type-Options: nosniff'); header('X-Frame-Options: SAMEORIGIN'); header('X-XSS-Protection: 1; mode=block'); header('Referrer-Policy: strict-origin-when-cross-origin'); } /* ── Sensitive file blocking ──────────────────────────────── */ public function block_sensitive_files(): void { if (ITK_Whitelist::allowed()) return; $options = get_option('itk_security', []); $uri = $_SERVER['REQUEST_URI'] ?? ''; if (!empty($options['protect_wp_includes'])) { if (preg_match('#^/wp-includes/[^/]+\.php$#i', $uri)) { $this->send_403($options, 'Access to this file is not allowed.'); } if (preg_match('#^/wp-admin/includes/#i', $uri)) { $this->send_403($options, 'Access to this file is not allowed.'); } if (preg_match('#^/wp-includes/theme-compat/#i', $uri)) { $this->send_403($options, 'Access to this file is not allowed.'); } if (preg_match('#/wp-includes/js/tinymce/langs/.+\.php#i', $uri)) { $this->send_403($options, 'Access to this file is not allowed.'); } if (preg_match('#(license\.txt|wp-config-sample\.php|readme\.html)$#i', $uri)) { $this->send_403($options, 'Access to this file is not allowed.'); } if (preg_match('#(?:^|/)\.(?!well-known)#', $uri)) { $this->send_403($options, 'Access to hidden files is not allowed.'); } } if (!empty($options['protect_uploads'])) { if (preg_match('#^/wp-content/uploads/.*\.(?:php[1-6]?|pht|phtml?)$#i', $uri)) { $this->send_403($options, 'PHP files are not allowed in the uploads directory.'); } } if (!empty($options['block_xmlrpc'])) { if (strpos($uri, 'xmlrpc.php') !== false) { $this->send_403($options, 'XML-RPC is disabled on this site.'); } } } /* ── Malicious query blocking ─────────────────────────────── */ public function block_malicious_queries(): void { if (ITK_Whitelist::allowed()) return; $options = get_option('itk_security', []); if (empty($options['block_malicious_queries'])) return; $qs = $_SERVER['QUERY_STRING'] ?? ''; if (empty($qs)) return; $patterns = [ '(eval\()', '(127\.0\.0\.1)', '([a-z0-9]{2000})', '(javascript:)(.*)(;)', '(base64_encode)(.*)(\()', '(GLOBALS|REQUEST)(=|\[|%)', '(<|%3C)(.*)script(.*)(>|%3)', '(boot\.ini|etc/passwd|self/environ)', '(thumbs?(_editor|open)?|tim(thumb)?)\.php', '(\'|\\")(.*)(drop|insert|md5|select|union)', ]; foreach ($patterns as $pattern) { if (preg_match('#' . $pattern . '#i', $qs)) { $this->send_403($options, 'Malicious query detected.'); } } $method = strtolower($_SERVER['REQUEST_METHOD'] ?? ''); if (preg_match('#^(connect|debug|delete|move|put|trace|track)$#', $method)) { $this->send_403($options, 'This request method is not allowed.'); } } /* ── Author scan blocking ─────────────────────────────────── */ public function block_author_scans(): void { if (ITK_Whitelist::allowed()) return; $options = get_option('itk_security', []); if (empty($options['block_author_scans'])) return; $uri = $_SERVER['REQUEST_URI'] ?? ''; $qs = $_SERVER['QUERY_STRING'] ?? ''; if (strpos($uri, '/wp-admin') === false && preg_match('/author=\d+/i', $qs)) { wp_redirect(home_url(), 301); exit; } } /* ── Custom login URL ─────────────────────────────────────── */ public function custom_login_url(): void { $options = get_option('itk_security', []); if (empty($options['enable_custom_login'])) return; $slug = $this->custom_slug($options); $path = parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?? ''; $qs = parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_QUERY) ?? ''; if (strpos($path, '/' . $slug) !== false) { if (!session_id()) session_start(); $_SESSION['itk_login_access'] = time(); require_once ABSPATH . 'wp-login.php'; exit; } $blocked = ['/wp-login.php', '/wp-admin/', '/login/', '/admin/']; foreach ($blocked as $b) { if (strpos($path, $b) !== false) { if (defined('DOING_AJAX') && DOING_AJAX) return; if (!session_id()) session_start(); if (isset($_SESSION['itk_login_access']) && (time() - $_SESSION['itk_login_access']) < 300) return; if (is_user_logged_in()) return; $this->send_403($options, 'Access denied. Please use the correct login URL.'); } } } public function wp_loaded_custom_login(): void { $options = get_option('itk_security', []); if (empty($options['enable_custom_login'])) return; global $pagenow; if ($pagenow === 'wp-login.php') { if (!session_id()) session_start(); if (!isset($_SESSION['itk_login_access'])) { $this->send_403($options, 'Access denied. Please use the correct login URL.'); } } } public function redirect_filter(string $location, int $status): string { $options = get_option('itk_security', []); if (empty($options['enable_custom_login'])) return $location; return str_replace('wp-login.php', $this->custom_slug($options), $location); } public function network_url_filter(string $url, string $path): string { $options = get_option('itk_security', []); if (empty($options['enable_custom_login'])) return $url; if (strpos($path, 'wp-login.php') !== false) { return str_replace('wp-login.php', $this->custom_slug($options), $url); } return $url; } public function site_url_filter(string $url, string $path): string { $options = get_option('itk_security', []); if (empty($options['enable_custom_login'])) return $url; if (strpos($path, 'wp-login.php') !== false) { return str_replace('wp-login.php', $this->custom_slug($options), $url); } return $url; } /* ── File upload filter ───────────────────────────────────── */ public function filter_uploaded_files(array $file): array { $options = get_option('itk_security', []); if (empty($options['protect_uploads'])) return $file; if (preg_match('/\.(php|phtml|php\d|pht|exe|dll|asp|aspx|jsp|cgi|pl)$/i', $file['name'] ?? '')) { $file['error'] = 'PHP and executable files cannot be uploaded.'; } return $file; } /* ── Helpers ──────────────────────────────────────────────── */ private function send_403(array $options, string $message): void { $code = $options['response_code'] ?? '403'; $redir = $options['redirect_url'] ?? ''; if ($code === '301_custom' && !empty($redir)) { header('Location: ' . esc_url_raw($redir), true, 301); } else { status_header(403); echo esc_html($message); } exit; } private function custom_slug(array $options): string { return !empty($options['custom_login_slug']) ? $options['custom_login_slug'] : 'thoushallpass'; } private function load_allowed_ips(): void { $defaults = ['127.0.0.1', '::1']; if (file_exists($this->allowed_ips_file)) { $lines = file($this->allowed_ips_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); foreach ($lines as $line) { $line = trim($line); if ($line !== '' && $line[0] !== '#') { $this->allowed_ips[] = $line; } } } if (empty($this->allowed_ips)) { $this->allowed_ips = $defaults; } } private function get_client_ip(): string { $keys = [ 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR', ]; foreach ($keys as $key) { if (empty($_SERVER[$key])) continue; $ip = trim(explode(',', $_SERVER[$key])[0]); if (filter_var($ip, FILTER_VALIDATE_IP)) return $ip; } return 'UNKNOWN'; } private function ip_in_cidr(string $ip, string $cidr): bool { [$subnet, $mask] = explode('/', $cidr, 2); if (!filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) return false; if (!is_numeric($mask) || $mask < 0 || $mask > 32) return false; if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) return false; $mask_dec = ~((1 << (32 - (int)$mask)) - 1); return (ip2long($ip) & $mask_dec) === (ip2long($subnet) & $mask_dec); } public function get_allowed_ips(): array { return $this->allowed_ips; } public function get_allowed_ips_file(): string { return $this->allowed_ips_file; } }