init_hooks(); $this->setup_security_headers(); } /** * Get instance * * @return TigerStyle_Life9_Security */ public static function instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } /** * Initialize security hooks */ private function init_hooks() { // Security headers add_action('send_headers', [$this, 'send_security_headers']); // Admin security add_action('admin_init', [$this, 'admin_security_check']); // AJAX security add_action('wp_ajax_tigerstyle_life9_*', [$this, 'verify_ajax_request'], 1); // File upload security add_filter('wp_handle_upload', [$this, 'secure_file_upload']); // Content security add_filter('wp_kses_allowed_html', [$this, 'filter_allowed_html'], 10, 2); } /** * Setup security headers */ private function setup_security_headers() { // Only apply to our plugin pages if (!$this->is_plugin_page()) { return; } // Content Security Policy $csp = "default-src 'self'; "; $csp .= "script-src 'self' 'unsafe-inline' 'unsafe-eval'; "; $csp .= "style-src 'self' 'unsafe-inline'; "; $csp .= "img-src 'self' data: blob:; "; $csp .= "font-src 'self'; "; $csp .= "connect-src 'self';"; header("Content-Security-Policy: $csp"); header('X-Content-Type-Options: nosniff'); header('X-Frame-Options: DENY'); header('X-XSS-Protection: 1; mode=block'); header('Referrer-Policy: strict-origin-when-cross-origin'); } /** * Send security headers */ public function send_security_headers() { if ($this->is_plugin_page()) { $this->setup_security_headers(); } } /** * Check if current page is a plugin page * * @return bool */ private function is_plugin_page() { global $pagenow; if (!is_admin()) { return false; } // Check for our plugin pages $plugin_pages = [ 'admin.php?page=tigerstyle-life9', 'admin.php?page=tigerstyle-life9-backup', 'admin.php?page=tigerstyle-life9-restore', 'admin.php?page=tigerstyle-life9-settings' ]; $current_page = $pagenow . '?' . $_SERVER['QUERY_STRING']; foreach ($plugin_pages as $page) { if (strpos($current_page, $page) !== false) { return true; } } return false; } /** * Verify CSRF token * * @param string $action Action to verify * @param string $nonce Nonce to verify * @return bool */ public function verify_nonce($action = null, $nonce = null) { $action = $action ?: self::NONCE_ACTION; $nonce = $nonce ?: $this->get_request_nonce(); if (!$nonce) { return false; } return wp_verify_nonce($nonce, $action); } /** * Create CSRF token * * @param string $action Action for the nonce * @return string */ public function create_nonce($action = null) { $action = $action ?: self::NONCE_ACTION; return wp_create_nonce($action); } /** * Get nonce from request * * @return string|null */ private function get_request_nonce() { // Check various possible nonce locations if (isset($_POST['_wpnonce'])) { return sanitize_text_field($_POST['_wpnonce']); } if (isset($_GET['_wpnonce'])) { return sanitize_text_field($_GET['_wpnonce']); } if (isset($_POST['tigerstyle_life9_nonce'])) { return sanitize_text_field($_POST['tigerstyle_life9_nonce']); } // Check headers $headers = getallheaders(); if (isset($headers['X-WP-Nonce'])) { return sanitize_text_field($headers['X-WP-Nonce']); } return null; } /** * Verify user capabilities * * @param string $capability Required capability * @return bool */ public function verify_capability($capability = null) { $capability = $capability ?: self::REQUIRED_CAPABILITY; return current_user_can($capability); } /** * Admin security check */ public function admin_security_check() { if (!$this->is_plugin_page()) { return; } // Verify user capability if (!$this->verify_capability()) { wp_die(__('🙀 Sorry! This territory is protected. You need proper cat credentials to access TigerStyle Life9 features.', 'tigerstyle-life9')); } } /** * Verify AJAX request security */ public function verify_ajax_request() { // Skip if not our AJAX call if (strpos($_REQUEST['action'], 'tigerstyle_life9_') !== 0) { return; } // Verify nonce if (!$this->verify_nonce()) { wp_send_json_error([ 'message' => __('🙀 Security check failed! This cat is suspicious of your request.', 'tigerstyle-life9'), 'code' => 'invalid_nonce' ]); } // Verify capability if (!$this->verify_capability()) { wp_send_json_error([ 'message' => __('🙀 Insufficient permissions! You need cat admin powers for this action.', 'tigerstyle-life9'), 'code' => 'insufficient_permissions' ]); } } /** * Secure file upload handler * * @param array $upload Upload data * @return array */ public function secure_file_upload($upload) { // Only process our uploads if (!$this->is_plugin_upload()) { return $upload; } $file_path = $upload['file']; $file_type = $upload['type']; // Validate file type $allowed_types = ['application/zip', 'application/x-tar', 'application/gzip']; if (!in_array($file_type, $allowed_types)) { $upload['error'] = __('🙀 Invalid file type! This cat only accepts backup archives (.zip, .tar, .gz).', 'tigerstyle-life9'); return $upload; } // Validate file size (max 2GB) $max_size = 2 * 1024 * 1024 * 1024; // 2GB if (filesize($file_path) > $max_size) { $upload['error'] = __('🙀 File too large! Even cats with 9 lives have storage limits.', 'tigerstyle-life9'); return $upload; } // Scan for malicious content if ($this->scan_file_for_threats($file_path)) { unlink($file_path); $upload['error'] = __('🙀 Suspicious file detected! This cat\'s security instincts are tingling.', 'tigerstyle-life9'); return $upload; } return $upload; } /** * Check if current upload is for our plugin * * @return bool */ private function is_plugin_upload() { return isset($_POST['tigerstyle_life9_upload']) || (isset($_GET['page']) && strpos($_GET['page'], 'tigerstyle-life9') === 0); } /** * Scan file for security threats * * @param string $file_path Path to file * @return bool True if threats found */ private function scan_file_for_threats($file_path) { // Basic threat patterns $threat_patterns = [ '/<\?php/', // PHP code '/eval\s*\(/', // eval() calls '/exec\s*\(/', // exec() calls '/system\s*\(/', // system() calls '/shell_exec\s*\(/', // shell_exec() calls '/passthru\s*\(/', // passthru() calls '/file_get_contents\s*\(/', // file_get_contents() calls '/file_put_contents\s*\(/', // file_put_contents() calls '/fopen\s*\(/', // fopen() calls '/base64_decode\s*\(/', // base64_decode() calls ]; // Read first 1MB of file for scanning $content = file_get_contents($file_path, false, null, 0, 1024 * 1024); foreach ($threat_patterns as $pattern) { if (preg_match($pattern, $content)) { return true; } } return false; } /** * Filter allowed HTML for our plugin content * * @param array $allowed_html Allowed HTML tags * @param string $context Context * @return array */ public function filter_allowed_html($allowed_html, $context) { if ($context !== 'tigerstyle_life9') { return $allowed_html; } // Allow specific HTML for our plugin $plugin_html = [ 'div' => [ 'class' => true, 'id' => true, 'data-*' => true ], 'span' => [ 'class' => true, 'id' => true ], 'p' => [ 'class' => true ], 'button' => [ 'class' => true, 'type' => true, 'disabled' => true, 'data-*' => true ], 'input' => [ 'type' => true, 'name' => true, 'value' => true, 'class' => true, 'disabled' => true, 'readonly' => true ], 'select' => [ 'name' => true, 'class' => true, 'disabled' => true ], 'option' => [ 'value' => true, 'selected' => true ], 'textarea' => [ 'name' => true, 'class' => true, 'rows' => true, 'cols' => true, 'disabled' => true, 'readonly' => true ] ]; return array_merge($allowed_html, $plugin_html); } /** * Sanitize array recursively * * @param array $data Data to sanitize * @return array */ public function sanitize_array($data) { if (!is_array($data)) { return sanitize_text_field($data); } $sanitized = []; foreach ($data as $key => $value) { $key = sanitize_key($key); $sanitized[$key] = is_array($value) ? $this->sanitize_array($value) : sanitize_text_field($value); } return $sanitized; } /** * Log security event * * @param string $event Event type * @param string $message Event message * @param array $context Additional context */ public function log_security_event($event, $message, $context = []) { if (!defined('WP_DEBUG') || !WP_DEBUG) { return; } $log_entry = [ 'timestamp' => current_time('mysql'), 'event' => $event, 'message' => $message, 'user_id' => get_current_user_id(), 'ip_address' => $this->get_client_ip(), 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field($_SERVER['HTTP_USER_AGENT']) : '', 'context' => $context ]; error_log('TigerStyle Life9 Security: ' . wp_json_encode($log_entry)); } /** * Get client IP address * * @return string */ private function get_client_ip() { $ip_headers = [ 'HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'REMOTE_ADDR' ]; foreach ($ip_headers as $header) { if (isset($_SERVER[$header]) && !empty($_SERVER[$header])) { $ip = sanitize_text_field($_SERVER[$header]); if (filter_var($ip, FILTER_VALIDATE_IP)) { return $ip; } } } return '0.0.0.0'; } /** * Rate limit check * * @param string $action Action being rate limited * @param int $limit Maximum attempts * @param int $window Time window in seconds * @return bool True if rate limit exceeded */ public function check_rate_limit($action, $limit = 10, $window = 300) { $ip = $this->get_client_ip(); $key = "tigerstyle_life9_rate_limit_{$action}_{$ip}"; $attempts = get_transient($key); if ($attempts === false) { set_transient($key, 1, $window); return false; } if ($attempts >= $limit) { $this->log_security_event('rate_limit_exceeded', "Rate limit exceeded for action: $action", [ 'action' => $action, 'attempts' => $attempts, 'limit' => $limit ]); return true; } set_transient($key, $attempts + 1, $window); return false; } }