Contexto
Auditoría de seguridad (s220) en POST /api/racks/visio/confirm — endpoint que toma la lista de masters (formas convertidas o subidas) y las persiste en la librería de stencils del tenant. Se identificaron 3 vulnerabilidades:
- Permiso faltante: confirm no comprobaba permisos; analyze sí. Un usuario autenticado sin permisos admin podía importar stencils (violación de RBAC).
- Path-traversal: parámetro
image_pathcon../podía resolver fuera deMEDIA_ROOTy mover un fichero arbitrario del servidor a la librería del tenant (LFI → mutación). - XSS persistente: SVG subidos o generados podían contener malware embebido (
<script>, handlers,javascript:URLs) sin sanitización.
Decisiones de remediación
Fix 1: Gate de permisos (racks:admin)
Cambio: Añadir require_perm(request, "racks", "admin") en confirm, idéntico a analyze.
Ratificación:
- Importar stencils es acción administrativa (afecta a toda la librería del tenant)
- analyzer ya exigía admin; confirm debe mantener paridad
- Migración de test:
test_racks_visio_confirm.pyfixture ahoraadmin_useren lugar deoperator_user
Ubicación: racks/api/library_files.py, línea ~130
Fix 2: Guard anti-path-traversal
Cambio original (s220): Validar image_path con realpath() + prefijo de MEDIA_ROOT antes de mover fichero.
real = os.path.realpath(temp_full_path)
if not real.startswith(os.path.realpath(settings.MEDIA_ROOT) + os.sep):
continue # Skip este master, no se persiste
Ratificación (s220):
realpath()resuelve todos los../y symlinks- Comparación con prefijo de
MEDIA_ROOTevita escala - Fallback seguro: master se ignora silenciosamente (no error 400, evita fingerprinting)
⚠️ Superado por v1.92.0 (commit
c2df243, #478): confinar aMEDIA_ROOTentero resultó insuficiente — unimage_pathapuntando asignage/<orgB>/…obackups/…(fuera deltemp_dirde la sesión pero dentro deMEDIA_ROOT) seguía resolviendo dentro del prefijo válido, y como el propio código usashutil.move, un master de OTRA organización se movía (y destruía) hacia la librería del tenant atacante. El guard correcto confina altemp_dirde la sesión actual, no aMEDIA_ROOT:real = os.path.realpath(temp_full_path) if not is_within_directory(temp_dir, real): continueUsa el helper
racks.utils.safe_fs.is_within_directory(véase [[entity—racks—service—safe-fs]]) en vez de comparar prefijos a mano. Mismo patrón aplicado enconfirm_restore_backupyracks/api/export/restore_domains.py::_ensure_media_fileen el mismo commit.
Ubicación: racks/api/library_files.py, línea ~212 (s220) → confinamiento estrechado a temp_dir en v1.92.0
Fix 3: Defensa en profundidad — sanitización en confirm
Cambio: Aplicar sanitize_svg_file() en el gate final de confirm, antes de mover a librería.
if filename.lower().endswith(".svg"):
try:
sanitize_svg_file(temp_full_path)
except ValueError:
continue # Skip este master
Ratificación:
- SVGs ya se sanitizan aguas arriba (en analyze y en task Huey), pero confirm es la puerta final
- Si la lógica anterior fallara o sesión se manipulara, esto atrapa residuo
- Second pass es bajo costo (fichero ya en disco, parse rápido)
Ubicación: racks/api/library_files.py, línea ~220
Impacto en API contrato
- ✅ No cambios en request payload
- ✅ No cambios en response payload (confirm sigue devolviendo 200 + imported list)
- ⚠️ Código 403 ahora posible (estaba implícito en require_perm, ahora explícito en schema)
Testing
Nueva suite: tests/api/test_visio_import_v2.py
test_operator_cannot_confirm: operator→403 (fix 1)test_confirm_blocks_path_traversal: intento de../se ignora silenciosamente, fichero externo no se mueve (fix 2)test_confirm_sanitizes_svg_before_persisting: SVG malicioso llega limpio a disco (fix 3)
v1.92.0 añade a tests/racks/test_tanda2_file_confinement.py el caso de un image_path que resuelve dentro de MEDIA_ROOT pero fuera del temp_dir de la sesión (asset de otra org) — antes pasaba el guard de s220, ahora se descarta.
Migración de test existente:
tests/api/test_racks_visio_confirm.py::TestConfirmVisioImportEmpty::test_empty_masters_is_noop_200- Fue:
operator_user→ 200 noop - Ahora:
admin_user→ 200 noop (alineado con nuevo contrato) - Nuevo caso en v2:
operator_user→ 403
- Fue:
Precedentes
- RLS en confirm de otros imports:
racks/api/import_*.pyendpoints de confirmación ya validaban permisos - Sanitización SVG: standard para user-supplied geometry (OWASP XSS Prevention Cheat Sheet)
- Realpath guards: best practice en operaciones de filesystem con paths de usuario — refinado en v1.92.0 a confinar al directorio de trabajo de la sesión, no a la raíz de medios completa
Impacto observacional
- Usuarios admin: ninguno — confirm sigue funcionando igual
- Usuarios operator/viewer: ahora reciben 403 en confirm, eran 200 noop antes (nunca lograban crear stencils porque estaban vacíos, pero no era por permisos)
- Security posture: cierre de 3 vías de ataque identificadas en auditoría (s220) + 1 vía residual cerrada en v1.92.0
Véase también
- [[feature—racks—import-visio-v2]]
- [[entity—racks—service—svg-sanitizer]]
- [[entity—racks—endpoint—visio-confirm]]
- [[entity—racks—endpoint—visio-analyze]]
- [[entity—racks—service—safe-fs]]
- [[concept—saas—security]]