up
This commit is contained in:
@@ -0,0 +1,575 @@
|
||||
<?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_settings.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();
|
||||
}
|
||||
?>
|
||||
Reference in New Issue
Block a user