4. Modelo de amenazas y mitigaciones (STRIDE)
ExpressMX adopta una postura Zero Trust por defecto: ningún componente asume confianza implícita en la red, todos los accesos a datos de cliente se autentican, autorizan y auditan. El modelo se documenta usando STRIDE con una mitigación por categoría.
Última actualización: 2026-05-05
4.1 Capas de seguridad
| Capa | Responsabilidad | Implementación |
|---|---|---|
| TLS edge | Cifrado en tránsito | TLS 1.3 terminado en Vercel, HSTS |
| JWT auth | Identidad del usuario | apps/web/src/lib/auth/session.ts, jose con JWT_SECRET ≥ 32 chars |
| RBAC permisos | Autorización por rol y permiso | roles_admin, permisos, roles_permisos |
| RLS Postgres | Aislamiento a nivel de fila | ALTER TABLE ... ENABLE ROW LEVEL SECURITY |
| Audit log | Trazabilidad y no repudio | logs_auditoria, append-only |
| Break-glass | Acceso controlado a PII de cliente | pins_soporte_cliente, casos_soporte_abiertos |
4.2 Trust boundaries explícitas
- Borde móvil ↔ backend. Las apps Expo y la web del cliente envían peticiones a través de Internet. Todo request es validado por el middleware JWT antes de llegar al handler.
- Backend ↔ base de datos.
DATABASE_URLse inyecta en runtime de Vercel; la conexión TLS pooled apunta al endpoint de Neon. La base es inalcanzable directamente desde Internet. - Admin ↔ PII de cliente. Un admin con permisos no puede consultar datos de un cliente sin un caso break-glass vigente abierto a partir de un PIN generado por el cliente.
4.3 Tabla STRIDE × Mitigación
| Categoría STRIDE | Amenaza concreta | Mitigación implementada | Evidencia |
|---|---|---|---|
| Spoofing | Cliente se hace pasar por otro usuario en la API | JWT HS256 firmado, JWT_SECRET ≥ 32 chars validado al startup; verifyToken rechaza tokens forjados |
apps/web/src/lib/auth/session.ts líneas 11-15 y 35-42; integración valida token forjado y expirado en apps/web/src/__tests__/integration/auth.test.ts |
| Tampering | Cliente envía campos extra para forzar rol=admin o cliente_id=otro |
Schemas Zod estrictos con strip silencioso; tests adversariales lo confirman | packages/validations/src/__tests__/mobile-register.test.ts ("rechaza rol=admin", strip de id, activo, avatar_url); mobile-orders.test.ts strip de cliente_id, prestador_id |
| Repudiation | Admin niega haber consultado o modificado datos de un cliente | logs_auditoria append-only registra cada mutación con actor, recurso y timestamp; casos_soporte_abiertos deja huella del agente que abrió el caso |
migrations/003_pins_soporte_cliente.sql (tabla casos_soporte_abiertos); patrón logAccion documentado en RBAC Zero Trust |
| Information Disclosure | Cliente A intenta leer recursos del cliente B vía /api/v1/mobile/orders/[id] |
requireOwner ejecuta SELECT 1 FROM <tabla> WHERE id = $1 AND usuario_id = $2; si no hay fila lanza NotFoundError para no leak de existencia. RLS habilitado en tablas sensibles |
apps/web/src/lib/auth/guards.ts líneas 73-79; test "email inexistente → 401 (no leak de existencia)" en auth.test.ts |
| Denial of Service | Atacante envía paginación absurda (limit=1000000) o cursor enorme |
Schemas Zod imponen limit ∈ [1,50], cursor ≤ 256 chars, notas ≤ 1000 chars, password ≤ 128 chars; rate limiting en edge Vercel |
packages/validations/src/__tests__/common.test.ts ("rechaza limit muy grande (DoS)", "rechaza cursor con tamaño excesivo (DoS)") |
| Elevation of Privilege | Cliente intenta acceder a endpoints de prestador o admin | requireRole valida session.rol contra lista permitida y retorna 403; rol Zero Trust lector_basico para nuevos admins; super_admin requiere asignación explícita de cada permiso |
migrations/004_rbac_permisos_zero_trust.sql líneas 62-70 (lector_basico); test "cliente intentando GET jobs → 403 FORBIDDEN" en provider-jobs.test.ts |
4.4 Mecanismo break-glass (Zero Trust)
El modelo prohíbe que un agente de soporte navegue listas de clientes. El flujo controlado es:
- El cliente genera un PIN desde su app móvil; el PIN se almacena
hasheado en
pins_soporte_clientey expira en 15 minutos (migrations/003_pins_soporte_cliente.sql). - El cliente entrega el PIN al agente por canal acordado.
- El agente lo introduce en la consola de soporte; la apertura crea una
fila en
casos_soporte_abiertosconagente_id,cliente_id,pin_id,expira_en(4 horas) yabierto_en. - El scope de las consultas del agente sobre datos del cliente queda atado al caso vigente; al cerrar o expirar el caso se pierde el acceso.
- La acción de apertura y cierre queda persistida y disponible para auditoría.
Permisos asociados (introducidos en migración 004):
soporte.abrir_caso— sólo para rolessoporte,agente_soporteysuper_admin.usuarios.cambiar_password— disponible vía caso vigente para rolessoporteysuper_admin.
4.5 Hardening adicional
JWT_SECRETse valida al startup; el proceso aborta si es inferior a 32 chars (apps/web/src/lib/auth/session.tslíneas 11-15).- Cookies de sesión
auth_tokenconHttpOnlyySameSite. - Tests de integración cubren escenarios adversariales: SQL injection en email, body como array, NoSQL injection style en password.
DATABASE_URLnunca se loguea; en CI se usa placeholder durante el build estático.- Rotación de secretos manual; pendiente formalizar runbook.
4.6 Gaps conocidos y trabajo pendiente
- No hay rate limiting estructurado por IP en la capa aplicación; sólo rely en límites Vercel.
- No hay encriptación a nivel columna para PII (depende del cifrado de disco de Neon).
- No hay revisión periódica automática de permisos asignados a
super_adminni de roles huérfanos. - Falta runbook formal de rotación de
JWT_SECRET.