Divulgación · Backend as a Service

Más de 280.000 contactos al alcance de cualquiera

Read in English

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_amountvolumen total de transferencias (ARS)
get_contacts_audience_counttamaño de la base de contactos
get_cost_summaryGastos del negocio
calculate_settlement_previewprevisualización de liquidación y comisión
get_contact_distinct_filterstaxonomí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

  1. Revocar EXECUTE de las funciones sensibles para el rol anónimo;
  2. autorizar dentro de la función (rol de staff), no confiar en el grant;
  3. devolver solo datos enmascarados y mínimos;
  4. activar RLS en facturación y cupones;
  5. agregar límite de intentos y rotar las claves publicadas.
← Volver a Seguridad