Disclosure · Backend as a Service · RLS
When the database is left wide open
The client was a company with an internal management system built on a Backend as a Service (BaaS, in the base44/Supabase style). These platforms expose the database directly to the frontend: security doesn't depend on the app's code but on the access rules configured per entity and per row. And that's where the hole was.
The pattern: "parent" entities protected, "children" not
Walking the API entities, some responded correctly (auth required, or empty via row rules). But several internal entities returned all their records with no authentication:
| Entity | No auth | Note |
|---|---|---|
| User / Config / PagoOrdenServicio | 401 | Correct |
| Cliente / Pedido / MovimientoCaja | 200 [] | Correct (RLS filters) |
| PedidoHistorial / PedidoProducto | 200 + data | No rule |
| PagoPedido / PagoOperacion | 200 + data | No rule |
| ChatMensaje / SeguimientoOperacion / DetalleOS | 200 + data | Missing access control |
The key detail: the protected entities (Cliente,
Pedido) were fine, but their related entities —the ones hanging off
them— had no rule. Anyone knowing the BaaS URL pattern reads the full data:
curl "https://<baas>/api/apps/<appId>/entities/PedidoHistorial"
# -> 200 OK
# [ { "pedido_id":"...", "usuario_email":"[redacted]@...",
# "estado_anterior":"Pendiente", "estado_nuevo":"Confirmado" }, ... ]
In total, over 13,000 internal records with personal and financial data: customer
emails, payment methods and amounts, and internal conversations. None were test
data (is_sample was false for 100% of them).
Why it's so easy to exploit
It needs no authentication, no user interaction, no special conditions: just an
HTTP request. And since the API responded with
Access-Control-Allow-Origin: *, almost any malicious site could read
that data from a visitor's browser and exfiltrate it. The frontend JavaScript
"gate" (role === "admin") protects nothing: it's cosmetic.
Root cause and fix
The cause is configuration, not code: row-level access rules weren't applied to all entities. The fix is to write those rules for all of them:
Entity: PedidoHistorial
read: authenticated AND (usuario_email == current_user.email OR current_user.role == "admin")
create/update/delete: authenticated AND current_user.role == "admin"
- Read: never "public/anonymous"; require an authenticated owner or admin.
- Write: authenticated + owner or admin.
- Verify after: repeat the same requests with no token and confirm they return
401or200 []— never200with data. - Restrict CORS to your own domain and keep internal costs out of the public catalog.
The lesson
On BaaS platforms the database is the attack surface. When access rules are defined per entity, it's very easy to protect the main tables and forget the child ones — which often hold the most sensitive data: payments, chats, history.