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_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 "🛡️ 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, '