Resumen
Cuarta tanda de la Auditoría Suprema del módulo network (sesión 115). Enfoque en la seguridad de credenciales, backups de configuración y grupos de dispositivos. Se cerraron 30 hallazgos confirmados (4 ALTA, 14 MEDIA, 12 BAJA) mediante unificación de gates de autorización y validaciones defensivas.
Sin cambios visibles para usuarios administradores. Es endurecimiento de permisos (readonly ya no ve secretos) + honestidad de la interfaz (restore_backup ya no finge éxito).
Hallazgos ALTA (4)
ALTA #1: Fuga de credenciales descifradas (get_device_credentials)
Problema: El endpoint /api/network/device/{device_id}/credentials devolvía usuario/contraseña SSH + enable en claro a cualquier usuario de la org, incluso con rol readonly (que tiene permiso network:view por defecto).
Impacto: Un usuario de solo lectura podía obtener las credenciales SSH vivas de cualquier dispositivo y conectarse directamente.
Fix: Gateado con require_perm(request, "network", "edit"). Paralelo al fix anterior en core (/credentials/{id}/decrypt — sa1).
Fuentes:
network/api/devices.py::get_device_credentials— línea 115tests/api/test_network_sa4.py::TestDeviceCredentialsGate— cubre readonly 403, operator 200
ALTA #2: Server-Side Request Forgery en force_check_device
Problema: El health check (POST /api/network/device/{device_id}/check) abría un socket socket TCP desde el servidor hacia cualquier IP en la config, sin validar que no fuera una dirección interna (RFC1918, loopback, metadata). Un atacante podía forzar escaneos de servicios internos.
Impacto: Exposición de servicios privados, pivoting potencial.
Fixes:
- Gate
require_perm(request, "network", "edit")(antes sin autenticación clara). - Guard
is_private_ip(ip_address)— rechaza loopback, RFC1918 (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), CGNAT (100.64.0.0/10). - Validación de puerto:
int(port)y rango 1–65535 (antes aceptaba “500” como string). - Timestamps aware (
timezone.now()en lugar dedatetime.now()).
Fuentes:
network/api/devices.py::force_check_device— líneas 276–328network/services/__init__.py— funciónis_private_ip(importada)tests/api/test_network_sa4.py::TestForceCheckDevice— cubre SSRF guard, port validation, readonly 403
ALTA #3: Lectura de configuraciones con secretos en claro
Problema: Tres endpoints permitían descargar/ver configuraciones en texto plano (incluyendo communities SNMP, enable secrets, material criptográfico):
view_backup(GET /api/network/backup/{backup_id}/view)compare_backups(POST /api/network/backup/compare)download_backup(GET /api/network/backup/{backup_id}/download)
Un usuario readonly (que tiene network:view por defecto) podía descargar la configuración completa.
Matiz importante: list_backups (solo metadatos, sin config_text) SÍ se queda en view — permite al readonly ver que existen backups.
Fixes:
- Todos los tres endpoints ->
require_perm(request, "network", "edit"). - Anti-DoS en
compare_backups: cap de 2 MB por lado antes dedifflib. - Sanitización en
download_backup: filename limpio (header injection prevention) +re.sub(r"[^A-Za-z0-9._-]", "_", ...).
Fuentes:
network/api/backups.py::view_backup— línea 75network/api/backups.py::compare_backups— línea 101network/api/backups.py::download_backup— línea 354tests/api/test_network_sa4.py::TestBackupReadGate— cubre readonly 403 en view/download, readonly 200 en list
ALTA #4: restore_backup finge éxito (Reglas 13/15)
Problema: El endpoint /api/network/backup/{backup_id}/restore respondía 200 {success: true, "restore initiated"} sin restaurar nada. Era un placeholder. Violaba Reglas 13 (no mentir sobre estado) y 15 (fallos explícitos).
Fix: Ahora devuelve 501 Not Implemented tras validar existencia/propiedad del backup. El mensaje es honesto: “Configuration restore is not implemented yet”.
Fuentes:
network/api/backups.py::restore_backup— línea 299tests/api/test_network_sa4.py::TestRestoreHonest— verifica 501 admin, 403 operator
Hallazgos MEDIA (14)
- Mutaciones de backup (
trigger_backup,save_backup,delete_backup): unified arequire_perm(..., "admin"). update_device_status/update_devices_status_batch: gateadas arequire_perm(..., "edit")(antes sin gate claro).list_device_groups(antes sin gate): ahorarequire_perm(..., "view")+ optimización N+1 (annotate(Count distinct)→ una query en lugar de 3 COUNTs por grupo).create_device_group/assign_devices_to_groups/delete_device_group: unificados arequire_perm(antes mezclabais_admin,request.user.roledirecto, sin-gate). Delete sigue siendoadmin.- Schema
DeviceGroupInputSchema:nameycoloracotados amax_length(100, 20 respectivamente) → evitaDataError500, genera 422 limpio. - Manejo de errores:
str(exc)retirado de handlers → mensajes genéricos (no revela detalles internos). Códigos 404/500 normalizados (declarados en decorator Ninja). save_backupanti-DoS: cap 5 MB enconfig_text.log_network_action: guard enlevel→ fallback a “INFO” si es inválido.
Hallazgos BAJA (12)
- Error handling en
get_device_credentials:logger.exception()en lugar destr(e). - AttributeError 500 con Agent-JWT/anónimo en
device_groups.py→ ahora blocked por gate. - Response schemas en decoradores Ninja completas (incluye 404).
- Timestamps:
timezone.now()aware en lugar dedatetime.now(). - Validaciones de entrada: puerto no negativo, config_text > 0 y < límite.
Cambios en files
network/api/backups.py
- Importa
require_perm(nuevo decore.utils). - Todos los endpoints listados en ALTA #3 + mutaciones gateadas.
- Error responses genéricas (no
str(e)).
network/api/devices.py
- Importa
require_perm,is_private_ip. get_device_credentials: gate aedit.force_check_device: gate + SSRF guard + port validation.- Varios endpoints (status, network-config) ahora gateados.
network/api/device_groups.py
- Importa
require_perm. - Unificación de gates:
list=view,create/assign=edit,delete=admin. - Query optimizada:
annotate(Count distinct)→ una sola en lugar de N+1.
network/api/common.py
DeviceGroupInputSchema: fields conmax_length.
network/utils.py
log_network_action: guard enlevel.
tests/api/test_network_sa4.py (nuevo)
- 19 tests que cubren todos los hallazgos ALTA + MEDIA.
- Fixtures:
_device_with_config,_backup,_client_for.
Seguridad: Resumen de cambios de permisos
Antes (caótico):
get_device_credentials: sin gate efectivo (vulnerable).force_check_device: sin gate.- Backup views: sin gate (vulnerable).
- Device groups: mezcla de
is_admin,request.user.role, sin-gate.
Después (unificado):
network:view → list_* endpoints (metadata sin secretos)
network:edit → lectura/mutación de credenciales + configs + device status
network:admin → mutaciones de backup + delete_device_group + force_check_device
Relación con anteriores
- ALTA #1 espeja el fix anterior en
core(/credentials/{id}/decrypt— sa1). - ALTA #2 implementa anti-SSRF análogo al de UPS (monitoring sa5).
- ALTA #3 es análogo a la protección de
config_texten general. - ALTA #4 cierra una brecha flagrante de honestidad (Regla 13).
Pendientes (deferred a PRs propias)
- Cifrado en reposo SNMPv3 (Plan Hardening E).
DeviceProfileOut.snmp_community(A4 de sa1 — exposición).asyncio→ Huey async entrigger_backup.
Véase también
- [[concept—security—credential-exposure]]
- [[concept—security—ssrf-prevention]]
- [[concept—saas—multi-tenancy]]
- [[decision—20260609—require-perm-unified-authorization]]
- [[entity—network—model—device]]