1 puntos por GN⁺ 2024-09-13 | 1 comentarios | Compartir por WhatsApp
  • En una revisión del backend de la app de citas móvil Feeld se encontraron 8 vulnerabilidades, incluyendo exposición de perfiles, lectura y modificación de mensajes y acceso a archivos adjuntos del chat; salvo la primera, todas correspondían a Broken Access Control del OWASP Top 10
  • Aunque los usuarios básicos solo ven información limitada en la interfaz de la app, al inspeccionar las respuestas con un proxy podían recibir información de nivel premium como edad, distancia, foto de perfil y streamUserId de quienes les habían dado “Like”
  • Varias vulnerabilidades se encadenaban usando identificadores como streamUserId, profileId, messageId y channelID obtenidos de otras respuestas de la API e insertados en parámetros de solicitudes, ampliando el alcance al acceso a mensajes, matches, perfiles, Likes y hasta el envío de mensajes en chats ajenos
  • Se confirmaron problemas en archivos adjuntos del chat tanto para fotos normales, fotos temporales de 5 a 15 segundos, videos normales y videos de una sola reproducción; algunas URL de Cloudinary y Stream CDN permitían acceso sin autenticación
  • FORTBRIDGE divulgó los hallazgos a Feeld el 8 de marzo de 2024; tras varias solicitudes de retrasar la publicación, Feeld respondió el 16 de agosto de 2024 que había implementado cambios para mitigar los puntos restantes, y el blog se publicó el 10 de septiembre de 2024

Alcance de las vulnerabilidades encontradas en Feeld

  • El objetivo fue Feeld, una app móvil de citas similar a Tinder y Bumble, con filtros por distancia, edad, género, pareja y ubicación
  • Los usuarios premium también pueden buscar por tipo de kink, escenarios de threesome/group y tipo de relación de interés
  • En la revisión de seguridad se confirmaron 8 vulnerabilidades
    • Exposición de información de perfil a usuarios no premium
    • Lectura de mensajes ajenos
    • Acceso sin autenticación a fotos y videos adjuntos del chat
    • Eliminación, restauración y modificación de mensajes ajenos
    • Actualización de información de perfil ajena
    • Recepción de “Like” desde perfiles arbitrarios
    • Envío de mensajes en chats ajenos
    • Visualización de matches ajenos
  • Salvo la primera, los demás problemas correspondían a la categoría Broken Access Control del OWASP Top 10

Información de perfil expuesta a usuarios no premium

  • Cuando un usuario básico ve en el menú Likes a quienes le dieron “Like”, solo se muestran el nombre y una foto borrosa
  • Al interceptar solicitudes y respuestas con una herramienta proxy como Burp, la respuesta incluía información al nivel de un usuario premium
    • Edad
    • Distancia
    • Foto de perfil completa
    • streamUserId
  • Las fotos de perfil estaban almacenadas en res.cloudinary.com y podían consultarse sin autenticación
  • El streamUserId obtenido en la respuesta podía usarse después para la vulnerabilidad de lectura de mensajes ajenos

Problemas de control de acceso en mensajes y matches

  • Para leer mensajes ajenos se necesitaba el streamUserId de la víctima, y este valor quedaba expuesto en varias solicitudes de la API
  • Un flujo de ejemplo consistía en obtener el streamUserId del usuario objetivo desde la respuesta de la solicitud GraphQL DiscoverProfiles y luego colocarlo en la condición member de una solicitud del canal de chat
  • Al buscar "text" en la respuesta se podía ver cuántos mensajes había intercambiado la víctima y su contenido
  • Con el mismo enfoque también se obtenía el messageId asociado a cada mensaje, valor usado para eliminar, restaurar o modificar mensajes
  • Al cambiar el parámetro vulnerable profileId de ChatListQuery se podían ver los matches de otros usuarios
    • La información visible incluía imaginaryName, edad, fotos, género, sexuality, status y fecha de nacimiento

Acceso sin autenticación a archivos adjuntos del chat

  • Los archivos adjuntos compartidos en el chat se dividían en fotos y videos
    • Las fotos podían ser fotos normales visibles o fotos temporales de 5 a 15 segundos
    • Los videos podían ser videos normales reproducibles o videos de una sola reproducción
  • Las fotos normales se subían desde la app de Feeld a api.cloudinary.com, y la respuesta devolvía un photo_id
    • Después la foto se copiaba a feeld.co y se entregaba a usuarios autenticados
    • Se usaban rutas con la forma cdn/chat-attachment/<receiver_profileId>/<photo_id> o <sender_profileId>/<photo_id>
    • Incluso reduciendo la parte de profileId de la ruta a una cadena arbitraria de al menos 1 carácter, la foto seguía devolviéndose a usuarios autenticados
    • La ruta con /v1/ al frente devolvía la URL de la foto original almacenada en Cloudinary, y esa URL podía abrirse sin autenticación
  • Las fotos temporales usaban al subirlas parámetros adicionales como visibilityMilliseconds:15000
    • El endpoint para el receptor eliminaba la foto entre 5 y 15 segundos después del acceso, por lo que ya no era accesible
    • El endpoint usando el profileId del uploader seguía devolviendo la foto a usuarios autenticados incluso después de 5 a 15 segundos
    • La ruta /v1/ devolvía una URL de Cloudinary accesible sin autenticación
  • Tanto en videos normales como en videos de una sola reproducción, la URL iba incluida en el mensaje del chat
    • Los videos normales se subían a us-east.stream-io-cdn.com
    • Los videos de una sola reproducción usaban un flujo de subida hacia chat.stream-io-api.com
    • Si un atacante obtenía la URL mediante la vulnerabilidad previa de lectura de mensajes y cambiaba u0026 por &, podía verla sin autenticación
  • Los videos de una sola reproducción podían volver a reproducirse para el atacante, mientras que en la app del receptor aparecían como video expired después de verse una vez

Manipulación de mensajes, cambio de perfil y falsificación de Likes

  • En el endpoint chat.stream-io-api.com/messages/<messageId>, los métodos DELETE y PUT permitían operar sobre mensajes ajenos
  • Los mensajes eliminados aparecían en el chat como This message was deleted, pero si el atacante llamaba la misma solicitud DELETE recibía de vuelta el mensaje original
  • El atacante podía modificar mensajes usando el messageId aunque no participara en el chat
    • Cuando la víctima tocaba la notificación, veía el mensaje modificado
    • Debajo del mensaje aparecía la marca edited, pero no se mostraba quién lo había editado
    • El nombre de la cuenta no era único y además podía cambiarse
  • Si en la solicitud GraphQL ProfileUpdate se reemplazaba el parámetro vulnerable id por el ID de la víctima, se podían actualizar datos del perfil como nombre, sexuality, edad y bio
  • En la solicitud GraphQL ProfileLike, iniciando sesión como profile#1 se podía hacer que profile#2 enviara un “Like” a profile#3
    • En el ejemplo, se envió un Like desde un perfil arbitrario hacia el propio perfil del investigador y ese Like aparecía en la lista de Likes de una cuenta premium

Envío de mensajes en chats ajenos

  • Un atacante podía enviar mensajes a chats de otras personas aunque no participara en ellos
  • El valor necesario era el channelID obtenido mediante la vulnerabilidad previa de lectura de mensajes
  • Al enviar una solicitud POST a la ruta channels/messaging/<channelID>/message, el mensaje se agregaba a ese canal
  • La víctima recibía una notificación y podía revisar el mensaje
  • El sistema mostraba la notificación como si proviniera del nombre del atacante, pero el atacante podía cambiar el nombre del perfil y los nombres no eran únicos

Cronología de la divulgación

  • El 8 de marzo de 2024, FORTBRIDGE divulgó todos los hallazgos a Feeld
  • Ese mismo día, Feeld pidió la información de las cuentas usadas en las pruebas
  • El 2 de abril de 2024, FORTBRIDGE solicitó una actualización y Feeld pidió retener la publicación mientras investigaba
  • El 28 de mayo de 2024, Feeld dijo que había desplegado varias correcciones y pidió un retraso de hasta 2 semanas para verificar si los hallazgos estaban resueltos
  • El 8 de junio de 2024 se cumplieron 3 meses desde el correo inicial de divulgación
  • El 15 de julio de 2024, Feeld respondió que algunos problemas requerían correcciones más complejas
  • El 4 de agosto de 2024, Feeld pidió posponer la publicación hasta resolver los puntos restantes
  • El 16 de agosto de 2024, Feeld respondió que había implementado cambios para mitigar los hallazgos restantes
  • El 8 de septiembre de 2024 se cumplieron 6 meses desde la divulgación inicial
  • El 10 de septiembre de 2024 se publicó el blog
  • En agosto de 2025, esta investigación se presentó en DEF CON 33

1 comentarios

 
GN⁺ 2024-09-13
Opiniones en Hacker News
  • Parece que implementaron la verificación de permisos solo en el frontend, y no solo en uno o dos endpoints, sino casi en todos lados
    Conceptualmente es un error fácil de evitar, pero he visto errores parecidos con demasiada frecuencia como para querer admitirlo
    La solución de “verificar todos los permisos en el backend” se siente parecida a la de los buffer overflows: “poner verificaciones de límites en todas partes”. Toda la comunidad sabe qué hay que hacer, pero lograr que todos lo apliquen de forma consistente no es fácil

    • No creo que sean lo mismo. La verificación de buffer overflows es un detalle de implementación y de lenguaje muy concreto, y puede ocurrir en cualquier parte de una base de código
      En cambio, la verificación de permisos ocurre en un límite específico y tiene que ver con cómo está diseñada la aplicación. Cada vez que pude influir en la forma de desarrollar un proyecto, insistí en separar claramente el desarrollo de la API del backend del código del cliente frontend. Por experiencia, eso hace que estos problemas sean mucho más fáciles de evitar y de probar, y además te da una API para desarrolladores “gratis”. Sinceramente, esa es la razón principal por la que prefiero este enfoque
    • Si alguien se confunde con esto, no debería tocar código del lado del servidor
    • Una vez descubrí a un desarrollador web haciendo autenticación en el frontend con un cuadro de diálogo común de JavaScript. Había metido la contraseña en el JS y hacía una comparación simple
      Me enteré porque el dueño de la cuenta lamp nos contactó diciendo que todos sus datos habían desaparecido de repente. Al revisar los logs, vimos que Google Bot había hecho clic en todos los enlaces “Delete” de la pantalla interna de administración. Fue posible porque JavaScript es opt-in. Llamé al desarrollador para explicarle lo que había hecho y ese día perdí mucha confianza en la gente de web
    • Creo que esto puede pasar muy fácilmente si usas una “API automática de DB” en el backend. Por ejemplo, me vienen a la mente algunas configuraciones automáticas de GraphQL
      Cada vez que lo veo lo marco, pero a veces se piensa demasiado poco en el alcance de la API del cliente, y eso me preocupa bastante
    • Lamentablemente es bastante común en apps móviles. Es la típica idea de “¿acaso el usuario va a inspeccionar una app móvil en detalle?”
      Me gustaría culpar a juniors, no-code o código hecho con IA, pero soy tan flojo como ellos, así que solo sacudo la cabeza y sigo adelante
  • Es una muy buena razón para no poner datos personales exactos. Por ejemplo, cosas como la fecha de nacimiento
    En especial las apps de citas parecen pedir este tipo de información, pero es mejor no hacerlo. Conviene poner un valor distinto, más o menos dentro de un año de tu cumpleaños real
    Esta app de citas no es muy conocida, pero está orientada a usuarios queer y a personas con otros gustos, como BDSM o sexo grupal. En muchas partes del mundo, sobra decir que esa información es muy sensible

  • Esta semana salió mucho en la prensa porque está ganando mucho dinero
    https://www.theguardian.com/technology/article/2024/sep/08/t...

    • Mucha gente ha visto últimamente que hacer cosas malas parece ser mucho más rentable que hacer cosas buenas
    • The Guardian debería ver esto
  • Dada la categoría de la app, es una falla al nivel de negligencia penal

    • Yo fui ese contratista barato. A los jefes no les importaba nada salvo los plazos y los bugs visibles para los revisores del cliente
      Creo que las únicas medidas disuasorias serían amenazas de cárcel en EE. UU. y la UE, seguros relacionados con datos y el costo de esos seguros. Si las fotos no son de las que se podrían subir a LinkedIn, deberían cobrar precios exorbitantes
      Por supuesto, los incentivos no deberían fomentar el encubrimiento
    • No era broma. Estas vulnerabilidades ya habrían sido vergonzosas hace 10 años
  • El sector de las citas online es un desastre. Solo hay 2 o 3 empresas con servicios que podrían considerarse útiles, y esas empresas son malvadas, incompetentes o ambas cosas
    Quizá ya haga falta algo como un servicio de citas federado y de código abierto. Al menos algo que no venda los datos, no filtre fotos de desnudos y no haga que la gente termine golpeada, violada o asesinada. No será tan fácil como decirlo, pero bueno

    • Llevo años pensando en algo así, pero no tengo suficiente dopamina libre como para construirlo además de mi trabajo principal
      ActivityPub incluso tiene la estructura para posibilitarlo mediante la publicación de registros Person. En especial, si se priorizan las necesidades de personas no monógamas, no heterosexuales y de género no conforme, hay muchísimo espacio para innovar
      Dicho eso, las apps de citas son un campo realmente difícil para entrar. Para que sean útiles se necesita una masa crítica de usuarios acumulada en una zona específica, y si las monetizas, inevitablemente la app se vuelve menos útil. Hay una razón por la que okcupid se arruinó después de dejar de tener un carácter sin fines de lucro
      Y también está el problema de la moderación
    • Siento que los desnudos deberían quedarse en formato analógico. Así puedes controlar su distribución de forma casi total y absoluta
      Si alguien quiere convertir una forma analógica en una copia digital, está en su derecho, pero debe saber que ningún sistema es ni será lo bastante seguro como para impedir filtraciones y distribución
      En especial la gente joven no considera las consecuencias y la vergüenza que pueden surgir, y que muy probablemente surgirán, a largo plazo. Ofrecer una función así no hace más que invitar consecuencias negativas
  • Es realmente horrible. Está claro que no pensaron en seguridad en absoluto
    Soy desarrollador de videojuegos, y nosotros dedicamos más esfuerzo a mantener nuestros juegos justos que esta empresa a mantener seguros a sus usuarios. Deberían destruirlos a demandas

    • Parece que no pensaron en seguridad ni en nada
      Incluso antes de darme cuenta de que la app estaba llena de bugs, me sorprendió mucho que la sección de intereses no ofreciera ningún contexto. Por ejemplo, casi todo el mundo tenía Domination o Submission como interés, pero no había ningún contexto sobre qué rol quería cada persona. No entender lo fundamentalmente mal que está eso en esa escena significa que, en general, no entienden nada
    • Hay que tener en cuenta que, en principio, los perfiles en una app de citas son accesibles para todos. Abres la app y aparecen perfiles. No hay nada como ACL
      Los mensajes y las fotos privadas son otro tema
  • Para decirlo de forma provocadora, esto es un problema de GraphQL.
    GraphQL permite que el frontend consulte datos. Es genial, pero desde la perspectiva del backend eso es muy opaco y normalmente se implementa con bibliotecas de terceros que no saben nada de control de acceso.
    Si no vas a implementar el control de acceso en la propia base de datos, en el código del backend es muy difícil desarmar una consulta GraphQL para determinar qué registros deben devolverse o restringirse. Hacerlo en la base de datos no es lo peor y definitivamente es mejor que hacerlo en el frontend.
    Para implementar un control de acceso correcto en el backend, necesitas entender la consulta, conocer el esquema de la base de datos y crear modelos, clases, funciones, etc. que determinen “si el user_id es XXX, ¿puede o no puede ver esta imagen en este contexto?”. En GraphQL es mucho más fácil implementarlo en el frontend, así que está claro que eso hicieron.
    No digo que la implementación de GraphQL haya sido buena, ni que el problema sea exclusivamente de GraphQL. Lo que quiero decir es que GraphQL intenta eliminar la necesidad de que el backend entienda la consulta, y eso hace más difíciles estas situaciones de seguridad complejas, por lo que vuelve más fácil cometer este error.
    [0] Por ejemplo, una imagen determinada puede ser accesible públicamente en el perfil de un usuario, pero visible solo para alguien con quien hizo match, o solo en el contexto de un chat (excluyendo chats grupales), o siempre inaccesible para un usuario bloqueado. Solo este caso ya puede generar un montón de casos límite complejos.

    • Es bastante fácil. Hay que tratar cada resolver que trae datos como un endpoint REST y protegerlo, y tener una lista de permitidos de consultas a la que se agreguen elementos durante el build de CI.
      No hace falta tocar el AST ni entender el contexto del resto de la consulta. En el resolver que trae fotos, basta con responder: “¿el usuario ABC puede ver las fotos del usuario XYZ?”. Si es ineficiente, se pueden precargar algunos datos o usar dataloader.
      Eso sí, si estás usando alguna biblioteca mágica que convierte GraphQL en SQL, la cosa cambia.
    • En GraphQL hay que definir permisos de acceso por atributo, o precompilar las consultas y ponerlas en una lista de permitidos. Con cualquier otra cosa, se filtran datos.
      https://hasura.io/docs/2.0/security/allow-list/
    • Cualquier biblioteca GraphQL de terceros que valga la pena debería implementar ACL de alguna forma. Parece que las más populares también lo hacen [1] [2].
      Una idea sencilla es implementar la autorización en el modelo de datos. Es decir, hacer que GraphQL delegue get y list en un modelo de recursos que pueda implementar la autorización según el contexto de la solicitud.
      [1] https://www.apollographql.com/docs/apollo-server/security/au...
      [2] https://docs.graphene-python.org/projects/django/en/latest/a...
    • Cuando usaba HotChocolate no tuve este problema. Es fácil asignar reglas de autorización a entidades o propiedades de entidades, y se procesan automáticamente. También se pueden aplicar a mutaciones.
  • Fue una divulgación sorprendentemente responsable y considerada.

    • ¿Incluyeron perfiles reales en el menú “Discover profiles” y en la captura de la lista de likes? Si es así, aunque hayan tapado las caras, es bastante irresponsable.
    • Sus acciones no coinciden con sus palabras.
  • No me sorprende demasiado. La uso, pero diría que está hecha con tanta incompetencia como mi app bancaria. Tal vez incluso peor; casi nunca funciona bien.
    No sé cómo la hicieron así.

    • Cuando la usé también era malísima. Si no eran fugas de memoria raras o problemas de privacidad, era una UX implementada de forma extremadamente deficiente.
      Al ver esta app y Fetlife, queda claro que esas comunidades tienen un problema serio: se quedan con la primera app que aparece, sin importar la calidad.
    • Cuando la usé, la comunidad era buena, pero la app nunca estuvo bien escrita.
      Luego, hace un tiempo, hicieron un flag day en el que desplegaron una app nueva y servidores nuevos para todos de una sola vez, y la mayoría ni siquiera podía iniciar sesión. Quienes lograban entrar, si eran clientes de pago, perdían sus beneficios premium, y hubo problemas como likes y chats que desaparecían. Yo nunca pude volver a iniciar sesión y en ese momento abandoné la app.
  • Sinceramente me sorprende que los investigadores hayan aguantado tanto antes de divulgarlo.
    Si le das 6 meses a una startup pésima para tapar un agujero de privacidad tan grave, seguirá abusando del privilegio de poder recopilar esta información en primer lugar. Creo que habría que darles solo 2 meses y luego publicarlo. Tienen que aprender que no se juega a los dados con la información privada de las personas.