575 lines
22 KiB
PHP
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)-\'',
|
|
'<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, '<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();
|
|
}
|
|
?>
|