db = Database::getInstance(); $this->originalSession = $_SESSION ?? []; } public function runAllSecurityTests() { echo "

🔒 Tests Exhaustivos de Seguridad

"; $this->testAuthentication(); $this->testAuthorization(); $this->testSessionSecurity(); $this->testInputValidation(); $this->testSQLInjectionPrevention(); $this->testXSSPrevention(); $this->testCSRFProtection(); $this->testRateLimiting(); $_SESSION = $this->originalSession; } private function testAuthentication() { echo "

🔐 Tests de Autenticación

"; $this->securityTest("Función isUserLoggedIn - usuario no logueado", function() { unset($_SESSION['admin_logged_in']); if (isUserLoggedIn()) { throw new Exception("isUserLoggedIn retorna true sin sesión activa"); } return "Usuario no logueado detectado correctamente"; }); $this->securityTest("Función isUserLoggedIn - usuario logueado", function() { $_SESSION['admin_logged_in'] = true; $_SESSION['last_activity'] = time(); if (!isUserLoggedIn()) { throw new Exception("isUserLoggedIn retorna false con sesión activa"); } return "Usuario logueado detectado correctamente"; }); $this->securityTest("Función requireAuthentication bloquea acceso", function() { unset($_SESSION['admin_logged_in']); ob_start(); try { requireAuthentication(); $output = ob_get_clean(); throw new Exception("requireAuthentication no bloqueó usuario no autenticado"); } catch (Exception $e) { $output = ob_get_clean(); // Verificar que hay salida JSON de error $decoded = json_decode($output, true); if (!$decoded || !isset($decoded['success']) || $decoded['success'] !== false) { throw new Exception("requireAuthentication no retorna JSON de error apropiado"); } return "requireAuthentication bloquea correctamente usuarios no autenticados"; } }); $this->securityTest("Timeout de sesión funciona", function() { $_SESSION['admin_logged_in'] = true; $_SESSION['last_activity'] = time() - (SESSION_TIMEOUT + 100); // Sesión expirada // Simular verificación de timeout (normalmente en config.php) if (isset($_SESSION['last_activity']) && (time() - $_SESSION['last_activity']) > SESSION_TIMEOUT) { unset($_SESSION['admin_logged_in']); } if (isUserLoggedIn()) { throw new Exception("Sesión expirada no fue invalidada"); } return "Timeout de sesión funciona correctamente"; }); $this->securityTest("Verificación de contraseña hash", function() { $testPassword = 'test123'; $hash = password_hash($testPassword, PASSWORD_DEFAULT); if (!password_verify($testPassword, $hash)) { throw new Exception("Verificación de hash falló"); } if (password_verify('wrong_password', $hash)) { throw new Exception("Hash acepta contraseña incorrecta"); } return "Sistema de hash de contraseñas funciona correctamente"; }); } private function testAuthorization() { echo "

👤 Tests de Autorización

"; $this->securityTest("APIs requieren autenticación", function() { $protectedAPIs = [ 'get_stats.php', 'get_users.php', 'send_message.php', 'save_template.php', 'get_templates.php', 'save_system_config.php' ]; unset($_SESSION['admin_logged_in']); foreach ($protectedAPIs as $api) { ob_start(); try { // Cambiar al directorio padre temporalmente $originalDir = getcwd(); chdir(__DIR__ . '/..'); include "api/$api"; // Restaurar directorio original chdir($originalDir); $output = ob_get_clean(); // Si no retorna error 401, falla el test if (strpos($output, '"success":false') === false) { throw new Exception("API $api no requiere autenticación"); } } catch (Exception $e) { ob_end_clean(); // Restaurar directorio en caso de excepción if (isset($originalDir)) { chdir($originalDir); } // Si hay excepción por no estar autenticado, está bien if (strpos($e->getMessage(), 'autenticación') === false && strpos($e->getMessage(), 'autorizado') === false) { throw $e; } } } return "APIs críticas requieren autenticación: " . count($protectedAPIs) . " APIs protegidas"; }); $this->securityTest("Panel principal redirige sin login", function() { unset($_SESSION['admin_logged_in']); // Simular acceso a index.php ob_start(); try { // El index.php ahora debería redirigir $_SERVER['REQUEST_URI'] = '/index.php'; // Verificar que la verificación está implementada $indexContent = file_get_contents('index.php'); if (strpos($indexContent, 'isUserLoggedIn()') === false) { throw new Exception("index.php no verifica autenticación"); } return "Panel principal protegido con verificación de login"; } finally { ob_end_clean(); } }); } private function testSessionSecurity() { echo "

🛡️ Tests de Seguridad de Sesión

"; $this->securityTest("Configuración de sesión segura", function() { $issues = []; // Verificar configuración de cookies if (!ini_get('session.cookie_httponly')) { $issues[] = "session.cookie_httponly debería estar habilitado"; } if (ini_get('session.cookie_secure') && !isset($_SERVER['HTTPS'])) { $issues[] = "session.cookie_secure habilitado sin HTTPS"; } if (ini_get('session.use_only_cookies') != 1) { $issues[] = "session.use_only_cookies debería estar habilitado"; } if (!empty($issues)) { return "⚠️ Mejoras recomendadas: " . implode(', ', $issues); } return "Configuración de sesión es apropiada"; }); $this->securityTest("Regeneración de ID de sesión", function() { session_start(); $oldId = session_id(); session_regenerate_id(true); $newId = session_id(); if ($oldId === $newId) { throw new Exception("ID de sesión no se regeneró"); } return "Regeneración de ID de sesión funciona"; }); $this->securityTest("Validación de IP de sesión", function() { $_SESSION['admin_logged_in'] = true; $_SESSION['login_ip'] = '192.168.1.1'; $currentIp = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1'; // En un sistema más seguro, esto debería validarse if (isset($_SESSION['login_ip']) && $_SESSION['login_ip'] !== $currentIp) { // Esto es opcional, pero es una buena práctica return "⚠️ Validación de IP no implementada (feature de seguridad adicional)"; } return "Validación de IP de sesión (básica)"; }); } private function testInputValidation() { echo "

✅ Tests de Validación de Entrada

"; $this->securityTest("Validación de número de teléfono", function() { $validNumbers = ['+573001234567', '573001234567', '57 300 123 4567']; $invalidNumbers = ['123', 'abc', '', '+1', '++573001234567', '', '"; DROP TABLE users; --', '../../../etc/passwd', '', 'javascript:alert(1)' ]; foreach ($maliciousInputs as $input) { // Test crear plantilla con entrada maliciosa try { $_SESSION['admin_logged_in'] = true; $testData = [ 'name' => $input, 'template_name' => 'safe_name', 'language_code' => 'es', 'category' => 'utility' ]; ob_start(); $_POST = $testData; $GLOBALS['HTTP_RAW_POST_DATA'] = json_encode($testData); include 'api/save_template.php'; $output = ob_get_clean(); $result = json_decode($output, true); // Si se creó exitosamente, verificar que se sanitizó if (isset($result['success']) && $result['success']) { $created = $this->db->fetch( "SELECT * FROM message_templates WHERE template_name = 'safe_name' ORDER BY id DESC LIMIT 1" ); if ($created && $created['name'] === $input) { // Limpiar $this->db->execute("DELETE FROM message_templates WHERE id = :id", ['id' => $created['id']]); throw new Exception("Entrada maliciosa '$input' no fue sanitizada"); } if ($created) { $this->db->execute("DELETE FROM message_templates WHERE id = :id", ['id' => $created['id']]); } } } catch (Exception $e) { ob_end_clean(); // Si rechaza entrada maliciosa, está bien continue; } finally { unset($_POST); unset($GLOBALS['HTTP_RAW_POST_DATA']); } } return "Entradas maliciosas son filtradas o rechazadas"; }); } private function testSQLInjectionPrevention() { echo "

💉 Tests de Prevención de Inyección SQL

"; $this->securityTest("Prepared statements en Database class", function() { $db = Database::getInstance(); // Test con entrada potencialmente maliciosa $maliciousId = "1'; DROP TABLE users; --"; try { $result = $db->fetch( "SELECT * FROM users WHERE id = :id", ['id' => $maliciousId] ); // Verificar que la tabla users sigue existiendo $tableCheck = $db->fetchAll("SHOW TABLES LIKE 'users'"); if (empty($tableCheck)) { throw new Exception("Tabla eliminada - posible inyección SQL exitosa"); } return "Prepared statements previenen inyección SQL básica"; } catch (Exception $e) { // Si hay error por tipo de dato incorrecto, está bien if (strpos($e->getMessage(), 'DROP') !== false) { throw new Exception("Inyección SQL detectada en error"); } return "Prepared statements rechazan entrada maliciosa"; } }); $this->securityTest("Validación en búsquedas de usuario", function() { $maliciousPhone = "'; SELECT password FROM admin_users; --"; try { $webhook = new WhatsAppWebhook(); $reflection = new ReflectionClass($webhook); $method = $reflection->getMethod('getUserByPhone'); $method->setAccessible(true); $result = $method->invokeArgs($webhook, [$maliciousPhone]); // No debería retornar datos de otras tablas if (is_array($result) && isset($result['password'])) { throw new Exception("Posible inyección SQL - datos no esperados retornados"); } return "Búsqueda de usuarios resistente a inyección SQL"; } catch (Exception $e) { // Error esperado con entrada maliciosa return "Búsqueda de usuarios rechaza entrada maliciosa"; } }); } private function testXSSPrevention() { echo "

🚫 Tests de Prevención XSS

"; $this->securityTest("Escape de salida en APIs", function() { $xssPayloads = [ '', '">', 'javascript:alert(1)', '\'-alert(1)-\'', '<script>alert(1)</script>' ]; // Test con API que retorna datos del usuario $_SESSION['admin_logged_in'] = true; foreach ($xssPayloads as $payload) { // Crear usuario con payload XSS $userId = $this->db->insert('users', [ 'phone_number' => '573000000001', 'name' => $payload, 'status' => 'active' ]); if ($userId) { ob_start(); include 'api/get_users.php'; $output = ob_get_clean(); $data = json_decode($output, true); if (is_array($data)) { foreach ($data as $user) { if (isset($user['name']) && $user['name'] === $payload) { // Limpiar $this->db->execute("DELETE FROM users WHERE id = :id", ['id' => $userId]); // En JSON, esto es relativamente seguro, pero verificar if (strpos($payload, '