Divulgación · Backend as a Service
Más de 280.000 contactos al alcance de cualquiera
Este no fue un cliente: fue una plataforma de apuestas online que revisé por interés público, ante prácticas que me parecieron cuando menos discutibles. Todo lo que sigue se obtuvo con la clave pública que el propio sitio entrega a cada visitante, sin crear cuenta, sin sesión y sin fuerza bruta. Solo peticiones de lectura.
La clave pública no es el problema: lo son las reglas
El sitio era una PWA sobre un Backend as a Service (BaaS). Que el frontend incluya la clave pública es normal en esa arquitectura: la seguridad no depende de esconder la clave, sino de las reglas de acceso (RLS) y de qué funciones quedan habilitadas para el rol anónimo. Y ahí estaba el problema.
1. Consultar a cualquier persona por su DNI, sin login
El checkout necesitaba autocompletar el nombre a partir del DNI. Para eso existía una función en el backend. El detalle: era ejecutable por cualquiera, y devolvía mucho más que el nombre.
POST https://<proyecto>.supabase.co/rest/v1/rpc/<rpc>
apikey: <clave_pública_del_sitio>
{"p_dni":"11111111"}
# -> [{
# "customer_name": "...",
# "full_email": "...",
# "full_phone": "...",
# "province": "...", "locality": "...", "address": "...",
# "postal_code": "...", "birth_date": "..."
# }]
La tabla de contactos tenía más de 280.000 registros. Enumerar DNI alcanzaba para cosechar nombre, email, teléfono, domicilio y fecha de nacimiento de la base completa — material ideal para phishing dirigido y robo de identidad. La misma función estaba expuesta en el entorno de producción y en el de staging.
2. Métricas financieras internas sin autenticación
Varias funciones de agregación aceptaban la clave pública y devolvían números internos del negocio:
| Función (sin auth) | Qué exponía |
|---|---|
bank_transfers_total_amount | volumen total de transferencias (ARS) |
get_contacts_audience_count | tamaño de la base de contactos |
get_cost_summary | Gastos del negocio |
calculate_settlement_preview | previsualización de liquidación y comisión |
get_contact_distinct_filters | taxonomía interna de fuentes y tags |
Lo interesante es que sus gemelas sí estaban protegidas
(Unauthorized). Es decir: no era comportamiento público
intencional, sino un olvido de permisos función por función.
3. Tablas internas legibles en el entorno de staging
En el proyecto de staging, el rol anónimo podía leer tablas internas: registros de facturación (montos, estado, quién lo cargó) y datos de cupones (códigos, usos máximos, variantes permitidas). El catálogo es discutible que sea público; los cupones y la facturación, no.
4. Lo que la vulnerabilidad no permitía
Un informe honesto también cierra puertas. Probé si el rol anónimo podía crear
o modificar una cuenta bancaria y marcarla como principal (lo que redirigiría
los pagos). Resultado: no era explotable. El RPC está concedido
al rol anónimo, pero valida autorización internamente y responde
Unauthorized; y las reglas de fila bloquean el alta y la edición.
Cierro el hallazgo como no vulnerable, sin inflarlo.
La lección
En un BaaS, cada función y cada tabla es una puerta. Que la clave pública esté
a la vista es esperable; que las funciones sensibles queden ejecutables por el
rol anónimo, no. Hay que revisar los permisos por objeto: revocar
EXECUTE del rol anónimo, exigir autorización dentro de la función,
y devolver el mínimo dato necesario (nunca el email o teléfono completo).
Remediación
- Revocar
EXECUTEde las funciones sensibles para el rol anónimo; - autorizar dentro de la función (rol de staff), no confiar en el grant;
- devolver solo datos enmascarados y mínimos;
- activar RLS en facturación y cupones;
- agregar límite de intentos y rotar las claves publicadas.