Volver a la wiki

Auditoría Suprema — Racks: 6 arreglos de seguridad ALTA (s104)

Resumen ejecutivo

Sesión: s104 (2026-06-02)
Ámbito: Dominio racks — librería, importación/restauración, gestión de racks
Severidad: ALTA (6 bugs confirmados, 3 con vector de explotación directo)
Estado: ✅ CERRADO (21 tests verdes, parche mergeado)
Origen: Auditoría Suprema (96 agentes, 5.83M tokens) — hallazgos de seguridad a fondo en el producto


Bugs ALTA arreglados

1. Borrado/edición cross-tenant de stencils globales

Código afectado: racks/api/library.py — delete_stencil(), update_stencil(), move_stencil()

Bug:

# ANTES (incorrecto)
stencil = Stencil.objects.get(
    Q(organization=org) | Q(organization__isnull=True),  # ← Q(organization__isnull=True)
    id=stencil_id,
)

Un tenant podía borrar/renombrar los stencils “globales del sistema” (aquellos con organization=None, compartidos por toda la plataforma). Bajo org=None (acceso de Agent JWT), operaba solo sobre los globales sin sesión humana.

Fix: Filtrar exclusivamente por organization=org + guarda if not org: return 400.

# DESPUÉS (correcto)
if not org:
    return 400, {"message": "No Organization Context"}
stencil = Stencil.objects.get(id=stencil_id, organization=org)

Impacto: Aislamiento multi-tenant enforced a nivel de ORM.

Endpoints corregidos:


2. Path traversal en cleanup_session + Zip Slip en restauración

Código afectado: racks/api/library.py (cleanup_session, confirm_visio_import, confirm_restore_backup, analyze_restore_backup) + racks/api/export/restore.py (restore_full)

Bug (Path Traversal):

# ANTES
@router.delete("/cleanup/session/{session_id}")
def cleanup_session(request, session_id: str):
    shutil.rmtree(f"{MEDIA_ROOT}/temp/{session_id}")  # ← session_id sin validar

Un atacante podía inyectar session_id = "../../etc" para borrar directorios arbitrarios del servidor.

Bug (Zip Slip):

# ANTES
with zipfile.ZipFile(zip_path, "r") as zipf:
    zipf.extractall(temp_dir)  # ← members sin sanear

# TAMBIÉN en restauración manual:
for name in zipf.namelist():
    target = os.path.join(settings.MEDIA_ROOT, name)  # ← no validado
    with open(target, "wb") as f:
        f.write(zipf.read(name))

Un ZIP malformado podía contener members como ../../../etc/passwd que escribirían fuera del destino (Zip Slip).

Fix: Nuevo módulo racks/utils/safe_fs.py con 3 helpers centralizados.

# DESPUÉS
if not is_valid_session_id(session_id):
    return 400, {"message": "Invalid session_id"}
# + safe_extract_zip() en análisis/restauración

Impacto:

Endpoints corregidos:


3. Gating de rol roto (ignora superuser, crashea con AnonymousUser)

Código afectado: racks/api/library.py — move_stencil(), analyze_visio(), delete_library_category(), rename_library_category()

Bug:

# ANTES (incorrecto)
if request.user.role != "admin":
    return 403, {"message": "Unauthorized"}

Fix: Usar la función centralizada is_admin(request.user) de core.api, que maneja is_superuser, TemporaryAccess, y AnonymousUser.

# DESPUÉS (correcto)
if not is_admin(request.user):
    return 403, {"message": "Unauthorized - Admin role required"}

Impacto: Gating de rol unificado y robusto en toda la app.

Funciones/endpoints corregidos:


4. delete_rack hacía hard delete, saltaba papelera

Código afectado: racks/api/racks.py — delete_rack()

Bug:

# ANTES
rack.delete()  # Hard delete → fila desaparece de la BD

El endpoint DELETE /api/racks/{id} borraba definitivamente el rack, saltándose el sistema de soft-delete (papelera/trash). Un borrado accidental era irreversible.

Fix: Usar rack.soft_delete() (método de modelo que setea deleted_at).

# DESPUÉS
rack.soft_delete()  # Soft delete → fila persiste, marked with deleted_at

Impacto: Borrados recuperables via papelera (modelo Rack ya tenía soporte con campo deleted_at y método soft_delete(), solo no se estaba usando).

Cambio de comportamiento en tests:


5. Gating de rol roto en gestión de categorías (mismo patrón)

Subsum en el bug #3 (gating con request.user.role != "admin"). Funciones:

Ambas reemplazan el chequeo con is_admin(request.user).


6. PUT /api/racks/bulk-update: endpoint roto (inexistente field)

Código afectado: racks/api/racks.py — endpoint bulk_update_racks() + schema BulkUpdateRacksSchema

Bug:

# ANTES
class BulkUpdateRacksSchema(Schema):
    rack_ids: list[int]
    category: str  # ← Rack.category NO EXISTE en el modelo

@router.put("/bulk-update")
def bulk_update_racks(request, payload):
    racks = Rack.objects.filter(id__in=payload.rack_ids)
    racks.update(category=payload.category)  # ← FieldError 500 en cada llamada

El endpoint intentaba escribir Rack.category, un campo inexistente en el modelo. Resultado: FieldError 500 en cada llamada (roto al 100%).

Fix: Remover el endpoint y el schema. La agrupación de racks ya se cubre con PUT /api/racks/groups/assign.

# DESPUÉS
# Removido completamente.
# Nota en el código:
# NOTE: el endpoint PUT /api/racks/bulk-update se retiro (s104):
# escribia Rack.category — un campo que NO existe en el modelo Rack — por lo que
# daba FieldError 500 en cada llamada (roto al 100%) y no tenia consumidores en el
# frontend. La agrupacion de racks se hace via PUT /api/racks/groups/assign.

Impacto:


Contexto de la auditoría

Auditoría Suprema (s104): Revisión a fondo del producto con 96 agentes + motor afinado. 13 ALTA confirmados en el dominio racks:

IDÁreaSeveridadEstadoNotas
s104.1Stencils globalesALTA✅ CERRADOCross-tenant delete/edit
s104.2Path traversalALTA✅ CERRADOcleanup_session UUID validation
s104.3Zip SlipALTA✅ CERRADOsafe_extract_zip + is_within_directory
s104.4Rol gatingALTA✅ CERRADOis_admin() central
s104.5Soft-deleteALTA✅ CERRADOdelete_rack → soft_delete()
s104.6Endpoint rotoALTA✅ CERRADORemover bulk-update
s104.7Backup asyncALTA⏳ BACKLOG (Etapa 3)Atomicidad de restore
s104.8Decompression bombALTA⏳ BACKLOG (Etapa 3)Límite de tamaño en ZIP
s104.9u_position validationALTA⏳ BACKLOG (Etapa 3)Range check Device.u_position
… (13 total)

Este commit: Cierra los 6 de mayor gravedad/esfuerzo S (mayor impacto vs. menor trabajo de parche).


Cobertura de tests

Nuevos tests (21 verde):

tests/api/test_racks_security.py (92 LOC)

tests/test_safe_fs.py (48 LOC)


Cambios en modelos/schema

TipoAntesDespuésRazón
SchemaBulkUpdateRacksSchema(removido)Endpoint roto (FieldError)
EndpointPUT /api/racks/bulk-update(removido)Sin consumidores, roto
ModeloRack.delete()Rack.soft_delete()Papelera en delete_rack
Helper(inexistente)racks/utils/safe_fs.pyPath traversal + Zip Slip guard

Notas de implementación

Deuda técnica preexistente

El archivo racks/api/library.py ya contenía 519 LOC lógicas (umbral Regla 5: >500). Este parche suma +1 LOC neto. Se bloquea pre-commit bajo [--no-verify autorizado por Edu].

Deuda anotada: Troceo de library.py + sentinel.py en backlog de modularización.

Patrones aplicables a otros dominios

Los 6 fixes en racks son aplicables a otros módulos:


CHANGELOG / RELEASE_NOTES

Documentado en:


Véase también

Subir