Divulgación · Control de acceso roto

Miles de usuarios expuestos y un storage abierto sin autenticación

Read in English

Este cliente era una plataforma con usuarios registrados y publicaciones de terceros. Lo interesante no fue un hallazgo, sino el patrón que se repetía una y otra vez: la autenticación se confundía con la autorización, y la validación de identidad se hacía en el cliente. El resultado: datos personales de miles de personas accesibles sin credenciales, y un bucket de almacenamiento donde cualquiera podía escribir.

1. PII completa de vendedores por un parámetro en la URL

El endpoint que devolvía el detalle de una publicación tenía dos vistas controladas por un flag: ?admin=true devolvía la versión "rica". Ese flag no verificaba nada.

# Sin credenciales, con el flag admin:
curl "https://api.<backend>/api/db/<endpoint>?id=<uid>&admin=true"
# -> { "name":"[redactado]", "email":"[redactado]", "phone":"[redactado]",
#      "<campo>": { "titular": { "cuit":"[redactado]", "nro_documento":"[redactado]" },
#                               "domicilio_titular": { "calle":"[redactado]", "numero":"[redactado]" } } }

Enumerando los UIDs públicos de las publicaciones y repitiendo la consulta se obtenía nombre, email, teléfono, CUIT, DNI y domicilio de todos los vendedores activos, sin ninguna credencial.

2. La lista de usuarios registrados con un registro gratuito

El endpoint /api/users/<endpoint> existía para el panel de administración. Pedía un token, pero no verificaba el rol. Como el alta de cuentas era abierta, cualquiera se registraba en segundos y obtenía la lista de usuarios:

# 1) Cuenta gratuita -> token válido
curl -X POST "https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=<api_key_publica>" \
  -d '{"email":"cualquiera@test.com","password":"Pass1234!","returnSecureToken":true}'

# 2) Con ese token, listar usuarios registrados (email + nombre + uid)
curl -H "Authorization: Bearer $TOKEN" "https://api.<backend>/api/users/<endpoint>"
# -> 1.000 usuarios

El endpoint devolvía de a 1000 y no aceptaba paginación, pero el impacto no era el límite: era que un usuario cualquiera podía leer el registro completo.

3. Emails de compradores escondidos en campos internos

El endpoint público de publicaciones traía, en cada aviso, un array con los emails de quienes habían hecho clic en "contactar por WhatsApp". Y el detalle individual traía otro tanto con los que habían marcado una publicación como favorito. Ninguno de los dos campos debía ser público:

OrigenDato expuesto sin auth
<endpoint> → <campo>[]email + nombre de cada interesado
<endpoint> → <campo>[]emails en texto plano de quienes marcaron favorito

El detalle técnico picante: el campo <campo> también aparecía en el listado masivo, y <campo> solo en el detalle individual. Un análisis que solo mira el listado se los pierde.

4. El bucket de Storage sin candado

El almacenamiento en la nube que servía todas las fotos (publicaciones y perfiles) no tenía reglas de seguridad efectivas. Sin ningún header de autenticación:

  • Lectura de cualquier objeto sin el token de descarga que el proveedor normalmente exige.
  • Escritura (POST) y sobrescritura de cualquier objeto.
  • Borrado (DELETE) de cualquier objeto.
# Escritura de un objeto arbitrario, sin credenciales
curl -X POST "https://firebasestorage.googleapis.com/v0/b/<proyecto>/o?name=pentest/poc.txt" \
  -H "Content-Type: text/plain" --data "test"
# -> 200, objeto creado

# Borrado del mismo objeto, sin credenciales
curl -X DELETE "https://firebasestorage.googleapis.com/v0/b/<proyecto>/o/pentest%2Fpoc.txt"
# -> 204

Subir contenido con Content-Type: text/html se servía inline, ejecutándose en el navegador bajo el dominio del proveedor. En la práctica: borrar la foto de cualquier publicación, o alojar phishing y XSS bajo una URL de Google que parece legítima.

Por qué pasaba

Dos causas raíz, la misma de fondo. Primero, el Origin como autenticación: la API rechazaba peticiones sin cabecera Origin y aceptaba Origin: https://<dominio>, que cualquiera puede falsificar con curl. Segundo, autorización decidida en el cliente: el panel admin verificaba el acceso comparando emails en JavaScript, y varias rutas no re-verificaban el rol del lado del servidor. Un control de cliente no es un control.

Impacto

  • PII de vendedores con datos de identidad (CUIT, DNI, domicilio): base para fraude y phishing dirigido.
  • Perfilado de interés: qué persona buscaba qué publicación, a qué precio.
  • Destrucción de contenido: borrar o desfigurar todas las fotos de la plataforma.
  • Marco legal: datos personales de terceros en Argentina, alcanzados por la Ley 25.326 de Protección de Datos Personales.

Remediación

  1. Eliminar la lógica de admin=true y separar en un endpoint que exija el rol verificado server-side.
  2. Verificar el rol (no solo la sesión) en <endpoint> y en toda ruta administrativa.
  3. Sacar <campo> y <campo> de las respuestas públicas; reemplazar por conteos.
  4. Escribir reglas de Storage que exijan autenticación para escritura y borrado, y restringir el Content-Type.
  5. Reemplazar Origin por validación real de tokens y activar el App Check del proveedor.
← Volver a Seguridad