Divulgación · Backend as a Service · RLS
Cuando la base de datos queda abierta
El cliente era una empresa con un sistema interno de gestión construido sobre un Backend as a Service (BaaS, del estilo base44/Supabase). Este tipo de plataformas expone la base de datos directamente al frontend: la seguridad no depende del código de la app, sino de las reglas de acceso que se configuren por entidad y por fila. Y ahí estaba el agujero.
El patrón: las entidades "padre" protegidas, las "hijas" no
Al recorrer las entidades de la API, algunas respondían correctamente (autenticación requerida, o vacío por reglas de fila). Pero varias entidades internas devolvían todos sus registros sin autenticación:
| Entidad | Sin auth | Observación |
|---|---|---|
| User / Configuración / PagoOrdenServicio | 401 | Correcto |
| Cliente / Pedido / MovimientoCaja | 200 [] | Correcto (RLS filtra) |
| PedidoHistorial / PedidoProducto | 200 + datos | Sin regla |
| PagoPedido / PagoOperacion | 200 + datos | Sin regla |
| ChatMensaje / SeguimientoOperacion / DetalleOS | 200 + datos | Falta control de acceso |
El detalle clave: las entidades protegidas (Cliente,
Pedido) estaban bien, pero sus entidades relacionadas —las que
cuelgan de ellas— quedaron sin regla. Cualquiera que conozca el patrón de URL
del BaaS lee los datos completos:
curl "https://<baas>/api/apps/<appId>/entities/PedidoHistorial"
# -> 200 OK
# [ { "pedido_id":"...", "usuario_email":"[redactado]@...",
# "estado_anterior":"Pendiente", "estado_nuevo":"Confirmado" }, ... ]
En total, más de 13.000 registros internos, con datos personales y financieros:
correos de clientes, métodos y montos de pago, y conversaciones internas.
Ninguno era dato de prueba (is_sample en false al 100%).
Por qué es tan fácil de explotar
No requiere autenticación, ni interacción del usuario, ni condiciones especiales:
es una simple petición HTTP. Y como la API respondía con
Access-Control-Allow-Origin: *, prácticamente cualquier sitio web
malicioso podría leer esos datos desde el navegador de un visitante y
exfiltrarlos. El "gate" en JavaScript del frontend
(role === "admin") no protege nada: es cosmético.
La causa y la corrección
La causa es de configuración, no de código: las reglas de acceso a nivel de fila no se aplicaron a todas las entidades. La corrección es escribir esas reglas para todas:
Entidad: PedidoHistorial
read: authenticated AND (usuario_email == current_user.email OR current_user.role == "admin")
create/update/delete: authenticated AND current_user.role == "admin"
- Lectura: nunca "público/anónimo"; exigir usuario autenticado y dueño o rol admin.
- Escritura: autenticado + dueño o admin.
- Verificación posterior: repetir las mismas peticiones sin token y confirmar que
devuelven
401o200 []— nunca200con datos. - Restringir CORS al dominio propio y sacar los costos internos del catálogo público.
La lección
En las plataformas BaaS la base de datos es la superficie de ataque. Si las reglas de acceso se definen por entidad, es muy fácil proteger las tablas principales y olvidar las hijas — que muchas veces tienen los datos más sensibles: pagos, chats, historial.