Files
whatsapp/tests/security_tests.php
T
2026-01-21 08:28:54 -05:00

575 lines
22 KiB
PHP

<?php
/**
* Tests de Seguridad para el Sistema WhatsApp Bot
* Fecha: 3 de enero de 2026
*/
class SecurityTestSuite {
private $db;
private $originalSession;
public function __construct() {
$this->db = Database::getInstance();
$this->originalSession = $_SESSION ?? [];
}
public function runAllSecurityTests() {
echo "<h2>🔒 Tests Exhaustivos de Seguridad</h2>";
$this->testAuthentication();
$this->testAuthorization();
$this->testSessionSecurity();
$this->testInputValidation();
$this->testSQLInjectionPrevention();
$this->testXSSPrevention();
$this->testCSRFProtection();
$this->testRateLimiting();
$_SESSION = $this->originalSession;
}
private function testAuthentication() {
echo "<h3>🔐 Tests de Autenticación</h3>";
$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 "<h3>👤 Tests de Autorización</h3>";
$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 "<h3>🛡️ Tests de Seguridad de Sesión</h3>";
$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 "<h3>✅ Tests de Validación de Entrada</h3>";
$this->securityTest("Validación de número de teléfono", function() {
$validNumbers = ['+573001234567', '573001234567', '57 300 123 4567'];
$invalidNumbers = ['123', 'abc', '', '+1', '++573001234567', '<script>'];
$whatsappService = new WhatsAppService();
$reflection = new ReflectionClass($whatsappService);
$formatMethod = $reflection->getMethod('formatPhoneNumber');
$formatMethod->setAccessible(true);
// Números válidos deberían formatarse
foreach ($validNumbers as $number) {
$result = $formatMethod->invokeArgs($whatsappService, [$number]);
if (empty($result) || !is_string($result)) {
throw new Exception("Número válido '$number' no se formateó correctamente");
}
}
// Números inválidos deberían ser rechazados o limpiados
foreach ($invalidNumbers as $number) {
try {
$result = $formatMethod->invokeArgs($whatsappService, [$number]);
if ($result === $number && strpos($number, '<script>') !== false) {
throw new Exception("Entrada maliciosa no fue filtrada");
}
} catch (Exception $e) {
// Error esperado para entradas inválidas
continue;
}
}
return "Validación de números de teléfono funciona";
});
$this->securityTest("Sanitización de entrada de plantillas", function() {
$maliciousInputs = [
'<script>alert("xss")</script>',
'"; DROP TABLE users; --',
'../../../etc/passwd',
'<?php system($_GET[c]); ?>',
'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 "<h3>💉 Tests de Prevención de Inyección SQL</h3>";
$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 "<h3>🚫 Tests de Prevención XSS</h3>";
$this->securityTest("Escape de salida en APIs", function() {
$xssPayloads = [
'<script>alert("xss")</script>',
'"><img src=x onerror=alert(1)>',
'javascript:alert(1)',
'\'-alert(1)-\'',
'&lt;script&gt;alert(1)&lt;/script&gt;'
];
// 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, '<script>') !== false) {
return "⚠️ XSS payload en JSON - seguro si se maneja correctamente en frontend";
}
}
}
}
// Limpiar
$this->db->execute("DELETE FROM users WHERE id = :id", ['id' => $userId]);
}
}
return "APIs JSON relativamente seguras contra XSS";
});
}
private function testCSRFProtection() {
echo "<h3">🔄 Tests de Protección CSRF</h3>";
$this->securityTest("Verificar headers de seguridad", function() {
$expectedHeaders = [
'X-Content-Type-Options: nosniff',
'X-Frame-Options: SAMEORIGIN',
'X-XSS-Protection: 1; mode=block'
];
// Verificar que config.php establece estos headers
$configContent = file_get_contents(__DIR__ . '/../config/config.php');
$foundHeaders = 0;
foreach ($expectedHeaders as $header) {
if (strpos($configContent, $header) !== false) {
$foundHeaders++;
}
}
if ($foundHeaders === 0) {
return "⚠️ Headers de seguridad no configurados";
}
if ($foundHeaders < count($expectedHeaders)) {
return "⚠️ Algunos headers de seguridad faltantes ($foundHeaders/" . count($expectedHeaders) . ")";
}
return "Headers de seguridad configurados correctamente";
});
$this->securityTest("Validación de origen de petición", function() {
// Test básico - en producción debería validar Referer/Origin
$_SERVER['HTTP_REFERER'] = 'http://evil-site.com';
$_SERVER['REQUEST_METHOD'] = 'POST';
// Por ahora, no hay validación CSRF implementada
return "⚠️ Protección CSRF no implementada (recomendado para producción)";
});
}
private function testRateLimiting() {
echo "<h3">⏱️ Tests de Limitación de Velocidad</h3>";
$this->securityTest("Sistema de intentos de login", function() {
$testIp = '192.168.1.100';
// Verificar que la función checkLoginAttempts existe
if (!function_exists('checkLoginAttempts')) {
throw new Exception("Función checkLoginAttempts no existe");
}
// Test inicial - debería permitir
$allowed = checkLoginAttempts($testIp);
if (!$allowed) {
// Limpiar intentos previos
clearLoginAttempts($testIp);
$allowed = checkLoginAttempts($testIp);
}
if (!$allowed) {
throw new Exception("IP limpia debería estar permitida");
}
// Registrar fallos hasta bloquear
for ($i = 0; $i < MAX_LOGIN_ATTEMPTS; $i++) {
recordFailedLogin($testIp);
}
// Ahora debería estar bloqueada
$blocked = checkLoginAttempts($testIp);
if ($blocked) {
throw new Exception("IP debería estar bloqueada tras múltiples fallos");
}
// Limpiar
clearLoginAttempts($testIp);
return "Sistema de rate limiting funciona correctamente";
});
$this->securityTest("Configuración de límites", function() {
if (!defined('MAX_LOGIN_ATTEMPTS') || MAX_LOGIN_ATTEMPTS > 10) {
return "⚠️ MAX_LOGIN_ATTEMPTS muy alto (recomendado: 3-5)";
}
if (!defined('LOGIN_LOCKOUT_TIME') || LOGIN_LOCKOUT_TIME < 300) {
return "⚠️ LOGIN_LOCKOUT_TIME muy bajo (recomendado: 5+ minutos)";
}
return "Configuración de rate limiting apropiada";
});
}
private function securityTest($name, $callable) {
try {
$result = $callable();
$alertClass = 'success';
$icon = 'shield-alt';
if (strpos($result, '⚠️') !== false) {
$alertClass = 'warning';
$icon = 'exclamation-triangle';
}
echo "<div class='alert alert-$alertClass'><i class='fas fa-$icon'></i> <strong>$name:</strong> $result</div>";
} catch (Exception $e) {
echo "<div class='alert alert-danger'><i class='fas fa-times-circle'></i> <strong>$name:</strong> " . $e->getMessage() . "</div>";
}
}
}
// No ejecutar si es incluido por el runner principal
if (basename($_SERVER['PHP_SELF']) === 'security_tests.php') {
$configPath = __DIR__ . '/../config/config.php';
if (file_exists($configPath)) {
require_once $configPath;
}
$suite = new SecurityTestSuite();
$suite->runAllSecurityTests();
}
?>