MAJOR — Lógica Rota y Escalada de Privilegios
Las reglas de NutriTrack Pro presentan dos vulnerabilidades críticas que permiten a cualquier usuario autenticado elevar su propio rol a company_admin o super_admin mediante un update bypass, además de exponer el email de todos los usuarios. El modelo de lectura de meals está roto para la colaboración nutricionista/empresa.
Checklist de 6 Puntos
🔴
1. The Update Bypass — Create vs Update divergen en campos sensibles
FALLO CRÍTICO
🟠
2. Authority Source — El rol se lee de user-provided data en meals
RIESGO MAYOR
🟠
3. Business Logic — Nutricionistas no pueden ver meals de sus clientes correctamente
RIESGO MAYOR
🟡
4. Storage Abuse — Sin límites de longitud en strings ni tamaño de arrays
DoS / Resource Exhaustion
🟣
5. Type Safety — Campos sin validación de tipo explícita
Menor
🔵
6. Field-Level vs Identity-Level — hasOnly presente, pero sin ownership en update de meals
PARCIALMENTE OK
Hallazgos Detallados
La regla create en /users/{userId} limita el rol a ['employee', 'nutritionist'], pero la regla update solo verifica hasOnly([...keys...]) y no restringe el valor del campo role. El set de keys permitidas en update incluye role sin validar el valor. Un atacante con cuenta de employee puede emitir un update({ role: "company_admin" }) o incluso role: "super_admin" y la regla lo permite. Esto habilita acceso total a reportes y datos de empresa.
// VULNERABLE — update actual:
allow update: if request.auth.uid == userId
&& request.resource.data.keys().hasOnly([..., 'role', ...]);
// ❌ No valida request.resource.data.role == resource.data.role
// FIX — bloquear cambio de rol salvo admin:
allow update: if request.auth.uid == userId
&& request.resource.data.keys().hasOnly(['name', 'avatarUrl', 'updatedAt']) // ← sacar 'role' del update normal
&& (!request.resource.data.diff(resource.data).affectedKeys().hasAny(['role', 'companyId']));
// Solo super_admin puede cambiar roles via Cloud Function con Admin SDK
🛠
Recomendación: Eliminar role y companyId de los campos actualizables por el propio usuario. Los cambios de rol deben hacerse únicamente mediante Cloud Functions con Admin SDK, nunca desde el cliente.
La regla de lectura de /meals/{mealId} permite al nutricionista leer cualquier meal de cualquier empleado de cualquier empresa, solo por tener el rol nutritionist. No hay verificación de que el nutricionista esté asignado a la empresa del dueño del meal. Esto viola el GDPR (acceso a datos de salud de clientes ajenos) y supone una brecha de confidencialidad grave en un SaaS multi-tenant.
🛠
Recomendación: Añadir verificación de companyId: el nutricionista solo puede leer meals cuyo resource.data.companyId == get(/users/$(request.auth.uid)).data.companyId.
Las reglas de /reports y /meals comprueban el rol leyendo get(/users/$(request.auth.uid)).data.role. Si un atacante consigue elevar su rol en el documento /users/{uid} (ver Finding #1), automáticamente obtiene acceso a reportes y puede crear nuevos reportes como si fuera nutricionista. La fuente de autoridad (Firestore document) es la misma que puede ser manipulada por el cliente.
🛠
Recomendación: Migrar los roles a Custom Claims del Firebase Auth token (request.auth.token.role). Los Custom Claims solo los puede establecer el Admin SDK, haciendo imposible la auto-escalada desde el cliente. Usar get() solo para datos no sensibles como companyId.
La regla de lectura de /users/{userId} permite a cualquier usuario autenticado leer el perfil completo de cualquier otro usuario, incluyendo el campo email. En un SaaS de salud, exponer emails de empleados a otros empleados de la misma empresa (o incluso de otras) es una violación de privacidad y puede incumplir el RGPD. Los nutricionistas de una empresa no deberían ver emails de otra empresa.
🛠
Recomendación: Restringir la lectura de /users a: el propio usuario, o usuarios de la misma empresa (resource.data.companyId == get(/users/$(request.auth.uid)).data.companyId). Considerar proyecciones de campos (field masks) para ocultar el email en lecturas de terceros.
Ninguna colección define límites de tamaño para campos de tipo string (name, title, avatarUrl). Un usuario malintencionado podría crear documentos con strings de MB de tamaño, agotando la cuota de almacenamiento gratuita o elevando costes de Firestore de forma desproporcionada (Resource Exhaustion / DoS).
🛠
Recomendación: Añadir validaciones como request.resource.data.name is string && request.resource.data.name.size() < 128 en create y update.
Los campos createdAt, updatedAt y companyId no validan su tipo. Un cliente podría enviar createdAt: 999 (int en vez de timestamp) o companyId: null, corrompiendo la integridad de los datos y rompiendo queries que esperan tipos concretos.
🛠
Recomendación: Usar validaciones de tipo explícitas: request.resource.data.createdAt is timestamp, request.resource.data.companyId is string.
Salida JSON Estructurada
{
"score": 2,
"summary": "Las reglas de NutriTrack Pro presentan una escalada de privilegios crítica (update bypass de rol), lógica de colaboración rota (nutricionistas acceden a datos de clientes ajenos), y una autoridad de roles delegada en documentos manipulables por el cliente. Score 2/5 (MAJOR).",
"findings": [
{
"check": "The Update Bypass",
"severity": "critical",
"issue": "update en /users permite cambiar 'role' a cualquier valor incluyendo company_admin/super_admin sin restricción de valor.",
"recommendation": "Eliminar 'role' de campos actualizables por el cliente. Gestionar roles únicamente via Admin SDK + Custom Claims."
},
{
"check": "Business Logic vs. Rules",
"severity": "major",
"issue": "Nutricionista puede leer meals de empleados de CUALQUIER empresa, violando el aislamiento multi-tenant.",
"recommendation": "Añadir comprobación de companyId en la regla de lectura de /meals."
},
{
"check": "Authority Source",
"severity": "major",
"issue": "Los roles se leen de documentos Firestore (manipulables por el cliente via update bypass). La fuente de autoridad debe ser el Auth token.",
"recommendation": "Migrar roles a Custom Claims (request.auth.token.role)."
},
{
"check": "Authority Source / PII",
"severity": "moderate",
"issue": "Emails de todos los usuarios visibles para cualquier usuario autenticado. Viola RGPD en contexto de datos de salud.",
"recommendation": "Restringir lectura de /users a mismo companyId o al propio usuario."
},
{
"check": "Storage Abuse",
"severity": "minor",
"issue": "Sin límites de longitud en strings. Riesgo de Resource Exhaustion / DoS vía almacenamiento excesivo.",
"recommendation": "Añadir .size() < N en validaciones de create/update."
},
{
"check": "Type Safety",
"severity": "minor",
"issue": "createdAt, updatedAt y companyId no tienen validación de tipo explícita. Riesgo de corrupción de datos.",
"recommendation": "Añadir 'is timestamp' e 'is string' en las reglas de create/update."
}
]
}