Divulgación · Backend as a Service · RLS

Cuando la base de datos queda abierta

Read in English

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:

EntidadSin authObservación
User / Configuración / PagoOrdenServicio401Correcto
Cliente / Pedido / MovimientoCaja200 []Correcto (RLS filtra)
PedidoHistorial / PedidoProducto200 + datosSin regla
PagoPedido / PagoOperacion200 + datosSin regla
ChatMensaje / SeguimientoOperacion / DetalleOS200 + datosFalta 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 401 o 200 [] — nunca 200 con 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.
← Volver a Seguridad