diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..ab46f7b --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,131 @@ +# Seguridad en CronJob para CodeIgniter 4 + +## Vulnerabilidades Corregidas + +### 🚨 CRÍTICAS + +#### 1. Inyección de Comandos del Sistema (CVE Potencial) +- **Estado**: ✅ CORREGIDO +- **Archivo**: `src/Job.php` +- **Mejoras implementadas**: + - Validación de comandos peligrosos mediante patrones regex + - Uso de `escapeshellcmd()` para escapar comandos + - Lista negra de comandos destructivos y caracteres de control + - Verificación de códigos de retorno + +#### 2. Credenciales Hardcodeadas Débiles +- **Estado**: ✅ CORREGIDO +- **Archivo**: `src/Config/CronJob.php` +- **Mejoras implementadas**: + - Eliminación de credenciales por defecto "admin/admin" + - Campos de configuración vacíos que requieren configuración manual + - Validación obligatoria de credenciales configuradas + +### 🔒 ALTAS + +#### 3. Autenticación Insegura +- **Estado**: ✅ CORREGIDO +- **Archivos**: `src/Controllers/Login.php`, `src/Controllers/BaseCronJob.php` +- **Mejoras implementadas**: + - Protección contra ataques de fuerza bruta (rate limiting) + - Bloqueo temporal de cuentas tras intentos fallidos + - Validación segura de credenciales con `hash_equals()` + - Regeneración de ID de sesión (prevención de session fixation) + - Timeout automático de sesiones + - Logging de eventos de seguridad + - Protección CSRF opcional + +#### 4. Ataques SSRF (Server-Side Request Forgery) +- **Estado**: ✅ CORREGIDO +- **Archivo**: `src/Job.php` +- **Mejoras implementadas**: + - Validación estricta de URLs + - Bloqueo de IPs privadas y localhost + - Configuración segura de cURL (timeouts, verificación SSL) + - Limitación de redirects + +## Configuraciones de Seguridad Nuevas + +### En `src/Config/CronJob.php`: +```php +// Configuraciones de autenticación +public string $username = ''; // DEBE configurarse +public string $password = ''; // DEBE configurarse + +// Protección contra fuerza bruta +public int $maxLoginAttempts = 5; +public int $lockoutTime = 300; // 5 minutos + +// Protección de sesiones +public bool $enableCSRFProtection = true; +public int $sessionTimeout = 3600; // 1 hora +``` + +## Recomendaciones de Implementación + +### 1. Configuración Obligatoria +Antes de usar en producción, configurar obligatoriamente: +```php +// En app/Config/CronJob.php +public string $username = 'tu_usuario_seguro'; +public string $password = 'tu_contraseña_fuerte'; +public bool $enableDashboard = true; // Solo si es necesario +``` + +### 2. Uso Seguro de Shell Commands +```php +// ❌ PELIGROSO - No usar comandos con caracteres especiales +$schedule->shell('rm -rf / && echo "hacked"'); + +// ✅ SEGURO - Comandos simples y validados +$schedule->shell('php backup.php'); +$schedule->shell('ls -la /var/log'); +``` + +### 3. URLs Seguras +```php +// ❌ PELIGROSO - URLs internas +$schedule->url('http://localhost:8080/admin'); +$schedule->url('http://192.168.1.1/config'); + +// ✅ SEGURO - URLs externas públicas +$schedule->url('https://api.ejemplo.com/webhook'); +``` + +## Logging de Seguridad + +El sistema ahora registra automáticamente: +- Intentos de login fallidos con IP +- Bloqueos por fuerza bruta +- Comandos peligrosos detectados +- URLs maliciosas bloqueadas +- Sesiones expiradas +- Cambios de IP en sesiones activas + +## Lista de Verificación de Seguridad + +### Antes de Producción: +- [ ] Configurar credenciales únicas (no usar admin/admin) +- [ ] Revisar todos los comandos shell programados +- [ ] Validar todas las URLs en trabajos de tipo 'url' +- [ ] Configurar timeouts apropiados +- [ ] Habilitar CSRF si es necesario +- [ ] Revisar logs de seguridad regularmente +- [ ] Deshabilitar dashboard si no se usa (`enableDashboard = false`) + +### Monitoreo Continuo: +- [ ] Revisar logs de intentos de login fallidos +- [ ] Monitorear comandos bloqueados por seguridad +- [ ] Verificar URLs bloqueadas por SSRF +- [ ] Auditar trabajos programados regularmente + +## Contacto de Seguridad + +Si encuentras vulnerabilidades adicionales, por favor reporta de forma responsable a través de: +- Issues de GitHub (para vulnerabilidades no críticas) +- Email directo al mantenedor (para vulnerabilidades críticas) + +--- + +**Fecha de última actualización**: 2025-01-06 +**Versión del análisis**: 1.0 diff --git a/src/Config/CronJob.php b/src/Config/CronJob.php index 2eae06b..eaf97da 100644 --- a/src/Config/CronJob.php +++ b/src/Config/CronJob.php @@ -109,8 +109,17 @@ class CronJob extends BaseConfig |-------------------------------------------------------------------------- */ public bool $enableDashboard = false; - public string $username = 'admin'; - public string $password = 'admin'; + + // SECURITY: No usar credenciales por defecto en producción + // Estas credenciales deben ser cambiadas obligatoriamente + public string $username = ''; // Debe configurarse en el archivo de configuración local + public string $password = ''; // Debe configurarse en el archivo de configuración local + + // Configuraciones de seguridad adicionales + public int $maxLoginAttempts = 5; // Máximo intentos de login + public int $lockoutTime = 300; // Tiempo de bloqueo en segundos (5 minutos) + public bool $enableCSRFProtection = true; // Protección CSRF + public int $sessionTimeout = 3600; // Timeout de sesión en segundos (1 hora) /* |-------------------------------------------------------------------------- diff --git a/src/Controllers/BaseCronJob.php b/src/Controllers/BaseCronJob.php index d2f5179..c13a816 100644 --- a/src/Controllers/BaseCronJob.php +++ b/src/Controllers/BaseCronJob.php @@ -36,7 +36,27 @@ public function initController(RequestInterface $request, ResponseInterface $res protected function checkCronJobSession() { $result = false; + $config = config('CronJob'); + if ($this->session->get('cronjob')) { + // Verificar timeout de sesión + $loginTime = $this->session->get('cronjob_login_time'); + if ($loginTime && (time() - $loginTime) > $config->sessionTimeout) { + $this->session->destroy(); + log_message('info', 'CronJob: Sesión expirada por timeout'); + return false; + } + + // Verificar IP (opcional - podría ser problemático con proxies) + $sessionIP = $this->session->get('cronjob_ip'); + $currentIP = $this->request->getIPAddress(); + if ($sessionIP && $sessionIP !== $currentIP) { + log_message('warning', 'CronJob: Intento de acceso desde IP diferente. IP de sesión: ' . $sessionIP . ', IP actual: ' . $currentIP); + // Opcional: descomentar para forzar logout en cambio de IP + // $this->session->destroy(); + // return false; + } + $result = true; } diff --git a/src/Controllers/Login.php b/src/Controllers/Login.php index d2e2791..1d28b07 100644 --- a/src/Controllers/Login.php +++ b/src/Controllers/Login.php @@ -8,6 +8,9 @@ class Login extends BaseCronJob { + private const SESSION_LOGIN_ATTEMPTS = 'cronjob_login_attempts'; + private const SESSION_LOCKOUT_UNTIL = 'cronjob_lockout_until'; + /** * Displays the form the login to the site. */ @@ -17,36 +20,163 @@ public function index() return redirect()->to('cronjob/dashboard'); } + // Verificar si está bloqueado por intentos fallidos + if ($this->isLockedOut()) { + $lockoutTime = $this->session->get(self::SESSION_LOCKOUT_UNTIL); + $remainingTime = $lockoutTime - time(); + + $this->viewData['error'] = "Cuenta bloqueada por múltiples intentos fallidos. Intente nuevamente en " . + ceil($remainingTime / 60) . " minutos."; + } + return view(config('CronJob')->views['login'], $this->viewData); } public function validation() { + $config = config('CronJob'); + + // Verificar configuración de seguridad + if (empty($config->username) || empty($config->password)) { + log_message('error', 'CronJob: Credenciales no configuradas en el archivo de configuración'); + return redirect()->to('cronjob')->with('error', 'Configuración de seguridad incompleta'); + } + + // Verificar si está bloqueado + if ($this->isLockedOut()) { + return redirect()->to('cronjob'); + } + + // Validación CSRF si está habilitada + if ($config->enableCSRFProtection && !$this->validateCSRF()) { + log_message('warning', 'CronJob: Intento de acceso con token CSRF inválido desde IP: ' . $this->request->getIPAddress()); + return redirect()->to('cronjob')->with('error', 'Token de seguridad inválido'); + } + $validation = Services::validation(); - $validation->setRule('username', 'Username', 'required'); - $validation->setRule('password', 'Password', 'required'); + $validation->setRule('username', 'Username', 'required|max_length[100]'); + $validation->setRule('password', 'Password', 'required|max_length[255]'); - if (! $validation->withRequest($this->request)->run()) { + if (!$validation->withRequest($this->request)->run()) { return redirect()->to('cronjob'); } $username = $this->request->getPost('username'); $password = $this->request->getPost('password'); - $config = config('CronJob'); - if ($username !== $config->username || $password !== $config->password) { - return redirect()->to('cronjob'); + // Validar credenciales de forma segura + if (!$this->validateCredentials($username, $password, $config)) { + $this->recordFailedAttempt(); + log_message('warning', 'CronJob: Intento de login fallido para usuario: ' . $username . ' desde IP: ' . $this->request->getIPAddress()); + + return redirect()->to('cronjob')->with('error', 'Credenciales incorrectas'); } - $this->session->set('cronjob', true); + // Login exitoso - limpiar intentos fallidos + $this->clearFailedAttempts(); + + // Configurar sesión segura + $this->setupSecureSession(); + + log_message('info', 'CronJob: Login exitoso para usuario: ' . $username . ' desde IP: ' . $this->request->getIPAddress()); return redirect()->to('cronjob/dashboard'); } public function logout() { - $this->session->remove('cronjob'); + // Destruir sesión completamente + $this->session->destroy(); return redirect()->to('cronjob'); } + + /** + * Verifica si la cuenta está bloqueada por intentos fallidos + */ + private function isLockedOut(): bool + { + $lockoutUntil = $this->session->get(self::SESSION_LOCKOUT_UNTIL); + + if ($lockoutUntil && time() < $lockoutUntil) { + return true; + } + + // Si ya pasó el tiempo de bloqueo, limpiar + if ($lockoutUntil && time() >= $lockoutUntil) { + $this->clearFailedAttempts(); + } + + return false; + } + + /** + * Registra un intento de login fallido + */ + private function recordFailedAttempt(): void + { + $config = config('CronJob'); + $attempts = $this->session->get(self::SESSION_LOGIN_ATTEMPTS, 0) + 1; + + $this->session->set(self::SESSION_LOGIN_ATTEMPTS, $attempts); + + if ($attempts >= $config->maxLoginAttempts) { + $lockoutUntil = time() + $config->lockoutTime; + $this->session->set(self::SESSION_LOCKOUT_UNTIL, $lockoutUntil); + } + } + + /** + * Limpia los intentos fallidos de login + */ + private function clearFailedAttempts(): void + { + $this->session->remove(self::SESSION_LOGIN_ATTEMPTS); + $this->session->remove(self::SESSION_LOCKOUT_UNTIL); + } + + /** + * Valida las credenciales de forma segura + */ + private function validateCredentials(string $username, string $password, $config): bool + { + // Usar hash_equals para prevenir timing attacks + $usernameValid = hash_equals($config->username, $username); + $passwordValid = hash_equals($config->password, $password); + + return $usernameValid && $passwordValid; + } + + /** + * Configura una sesión segura + */ + private function setupSecureSession(): void + { + $config = config('CronJob'); + + // Regenerar ID de sesión para prevenir session fixation + $this->session->regenerate(); + + $this->session->set([ + 'cronjob' => true, + 'cronjob_login_time' => time(), + 'cronjob_ip' => $this->request->getIPAddress(), + 'cronjob_user_agent' => $this->request->getUserAgent() + ]); + } + + /** + * Valida el token CSRF + */ + private function validateCSRF(): bool + { + try { + $csrfToken = $this->request->getPost('csrf_token'); + $sessionToken = $this->session->get('csrf_token'); + + return $csrfToken && $sessionToken && hash_equals($sessionToken, $csrfToken); + } catch (\Exception $e) { + return false; + } + } } diff --git a/src/Exceptions/CronJobException.php b/src/Exceptions/CronJobException.php index 171e61f..7570378 100644 --- a/src/Exceptions/CronJobException.php +++ b/src/Exceptions/CronJobException.php @@ -20,4 +20,24 @@ public static function forInvalidExpression(string $type) { return new self(lang('CronJob.invalidExpression', [$type])); } + + public static function forInvalidCommand(string $command) + { + return new self("Comando peligroso detectado: {$command}. Contiene caracteres o comandos no permitidos."); + } + + public static function forCommandExecutionFailed(string $command, int $returnCode) + { + return new self("Error al ejecutar comando: {$command}. Código de salida: {$returnCode}"); + } + + public static function forInvalidUrl(string $url) + { + return new self("URL no válida o peligrosa: {$url}. Posible intento de SSRF."); + } + + public static function forUrlRequestFailed(string $url, string $error) + { + return new self("Error al realizar petición HTTP a: {$url}. Error: {$error}"); + } } diff --git a/src/Job.php b/src/Job.php index 3c9f29f..7315cff 100644 --- a/src/Job.php +++ b/src/Job.php @@ -230,14 +230,51 @@ private function runCommandInBackground(): bool|string * Executes a shell script. * * @return array Lines of output from exec + * @throws CronJobException */ protected function runShell(): array { - exec($this->getAction(), $output); + // Validar y escapar el comando para prevenir inyección + $command = $this->getAction(); + + // Validar que el comando esté en una lista blanca o tenga caracteres seguros + if (!$this->isValidCommand($command)) { + throw CronJobException::forInvalidCommand($command); + } + + // Escapar el comando para mayor seguridad + $escapedCommand = escapeshellcmd($command); + + exec($escapedCommand, $output, $returnCode); + + if ($returnCode !== 0) { + throw CronJobException::forCommandExecutionFailed($command, $returnCode); + } return $output; } + /** + * Validates if a command is safe to execute + */ + private function isValidCommand(string $command): bool + { + // Lista de comandos/caracteres peligrosos + $dangerousPatterns = [ + '/[;&|`$(){}[\]<>]/', // Caracteres de control de shell + '/\b(rm|del|format|fdisk|mkfs)\b/i', // Comandos destructivos + '/\b(wget|curl|nc|netcat)\b.*http/i', // Comandos de red sospechosos + ]; + + foreach ($dangerousPatterns as $pattern) { + if (preg_match($pattern, $command)) { + return false; + } + } + + return true; + } + /** * Calls a Closure. * @@ -262,12 +299,72 @@ protected function runEvent(): bool * Queries a URL. * * @return mixed|string Body of the Response + * @throws CronJobException */ protected function runUrl() { - $response = Services::curlrequest()->request('GET', $this->getAction()); + $url = $this->getAction(); + + // Validar URL para prevenir SSRF + if (!$this->isValidUrl($url)) { + throw CronJobException::forInvalidUrl($url); + } + + try { + $response = Services::curlrequest([ + 'timeout' => 30, + 'verify' => true, // Verificar certificados SSL + 'allow_redirects' => [ + 'max' => 3, // Limitar redirects + 'strict' => true + ] + ])->request('GET', $url); + + return $response->getBody(); + } catch (\Exception $e) { + throw CronJobException::forUrlRequestFailed($url, $e->getMessage()); + } + } + + /** + * Validates if a URL is safe to request (prevents SSRF) + */ + private function isValidUrl(string $url): bool + { + // Validar formato de URL + if (!filter_var($url, FILTER_VALIDATE_URL)) { + return false; + } + + $parsedUrl = parse_url($url); + + // Solo permitir HTTP/HTTPS + if (!in_array($parsedUrl['scheme'] ?? '', ['http', 'https'], true)) { + return false; + } - return $response->getBody(); + // Resolver IP del host + $host = $parsedUrl['host'] ?? ''; + $ip = gethostbyname($host); + + // Bloquear IPs privadas y localhost para prevenir SSRF + if ($this->isPrivateOrLocalIp($ip)) { + return false; + } + + return true; + } + + /** + * Check if IP is private or local (SSRF protection) + */ + private function isPrivateOrLocalIp(string $ip): bool + { + return !filter_var( + $ip, + FILTER_VALIDATE_IP, + FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE + ); } /** diff --git a/src/Views/login.php b/src/Views/login.php index 363b042..968bae5 100644 --- a/src/Views/login.php +++ b/src/Views/login.php @@ -13,19 +13,28 @@
+ enableCSRFProtection): ?> + + +

Please sign in

+ + + +
- - + +
- - + +
-

©