Saltar al contenido
← Volver al blog
seguridadapps

Un usuario ve datos de otro (IDOR): cómo encontrarlo y arreglarlo

Cómo probar tu app con dos cuentas para detectar IDOR, por qué aparece tanto en apps hechas con IA y cómo cerrarlo con verificaciones en el servidor y RLS.

Por Equipo Devonauts6 min de lectura
Read in English →
Una pared de buzones amarillos numerados, cada uno con su propia cerradura

Cuando un usuario puede abrir los registros de otro con solo cambiar un ID en la URL o en una petición, tu app tiene un IDOR (referencia directa insegura a objetos): el servidor devuelve lo que le pidan sin comprobar quién lo pide. La solución es que el servidor o la base de datos decidan a quién pertenece cada dato a partir de la sesión, en cada lectura y escritura; por ejemplo, con políticas de Row Level Security de Supabase que comparan auth.uid() con el dueño de la fila, en lugar de confiar en el ID que manda el navegador o en que la interfaz esconde el enlace.

En esta guía verás cómo probar tu propia app con dos cuentas, por qué las apps hechas con herramientas de IA caen tanto en esto, cómo se arregla en Supabase y en una API propia, y qué hacer si hubo usuarios reales afectados.

¿Cómo se ve un IDOR en la práctica?

Un usuario abre /facturas/1042 y ve su factura. Cambia la dirección a /facturas/1041 y ve la de otra persona. O la app carga los datos con una petición como GET /rest/v1/orders?user_id=eq.abc123, y al poner otro user_id aparecen los pedidos de ese otro usuario.

No hace falta ninguna herramienta especial. Los datos están a una petición editada de distancia, y por eso OWASP pone el control de acceso roto como el riesgo web número uno.

Y no se trata solo de leer. La misma falla suele permitir modificar o borrar registros ajenos, o crear registros a nombre de otra cuenta.

¿Por qué pasa tanto en apps hechas con IA?

Porque el código que hace que la vista previa se vea bien y el código que protege los datos viven en lugares distintos, y la IA casi siempre escribe solo el primero.

  • El frontend filtra con .eq('user_id', user.id), así que en pantalla cada quien ve solo lo suyo. Pero ese filtro corre en el navegador, y el navegador lo controla el usuario.
  • Las tablas de Supabase se crearon con Row Level Security apagado, o con una política amplia como using (true) para "arreglar" una lista vacía.
  • Un endpoint propio toma el userId del cuerpo de la petición o de la URL, no del token de sesión.
  • Los datos de administración están en la misma tabla que los de usuarios, y la diferencia solo se aplica escondiendo la página de admin.

Los IDs con apariencia aleatoria (UUID) hacen más difícil adivinar, pero no son la solución. Los IDs se filtran en enlaces compartidos, correos, registros y capturas.

¿Cómo pruebo mi propia app?

Usa dos cuentas de prueba tuyas, con datos distintos en cada una. Si tienes un entorno de pruebas, hazlo ahí.

  1. Entra como usuario A y crea un registro de cada tipo que tenga tu app (un proyecto, una factura, un mensaje, un archivo).
  2. Abre las herramientas de desarrollo, ve a la pestaña Network y anota las peticiones que cargan o cambian esos registros. Copia sus IDs.
  3. Entra como usuario B en una ventana privada e intenta llegar a los registros de A: pega las URL y repite las peticiones copiadas con la sesión de B (la mayoría de los navegadores permite copiar una petición como fetch o cURL con clic derecho).
  4. Prueba también las escrituras: modificar, borrar y crear un registro poniendo a A como dueño.
  5. Si usas Supabase, revisa el Table Editor, que marca las tablas con Row Level Security apagado. Cualquiera de ellas con datos de usuarios queda expuesta a quien tenga tu clave pública.

Cada intento debe fallar con un error o un resultado vacío. Si alguno devuelve datos de A, encontraste un IDOR.

¿Cómo lo arreglo en Supabase?

Activa Row Level Security en cada tabla con datos de usuarios y escribe una política para cada operación. Para una tabla donde cada fila pertenece a un usuario:

alter table public.invoices enable row level security;

create policy "Owners can read their invoices"
on public.invoices for select
to authenticated
using ((select auth.uid()) = user_id);

create policy "Owners can update their invoices"
on public.invoices for update
to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);

Agrega políticas de insert (con with check) y de delete solo si los usuarios deben poder hacer eso. La cláusula with check es clave: sin ella, alguien podría modificar una fila suya y cambiarle el user_id por el de otra persona.

Dos cosas más para revisar:

  • Datos compartidos. Si los registros pertenecen a un equipo o empresa, la política debe comprobar la membresía (por ejemplo, que exista una fila en una tabla memberships que una al usuario con esa empresa). Lo vemos en multi-empresa con Supabase.
  • Vistas y funciones. Las vistas y las funciones security definer pueden saltarse las políticas. Verifica que apliquen la misma regla de pertenencia.

La documentación de Supabase sobre Row Level Security explica la sintaxis, y en nuestros ejemplos de políticas RLS hay más patrones.

¿Cómo lo arreglo en una API propia?

Si tu app tiene su propio backend o funciones serverless, aplica la misma regla en el código:

  1. Toma la identidad del usuario de la sesión o del token verificado, nunca del cuerpo, de la URL ni de un encabezado que ponga el cliente.
  2. Carga el registro y compara su dueño (o su empresa) con esa identidad antes de devolverlo o cambiarlo.
  3. Responde igual, "no encontrado", tanto si el registro no existe como si es de otra persona, para que no se puedan sondear IDs.
  4. Pon esa verificación en una sola función compartida que usen todos los endpoints, en lugar de repetirla a mano.

¿Cómo confirmo que quedó arreglado?

Repite la prueba con dos cuentas en cada tabla y endpoint, incluidas las escrituras. Vuelve a hacerla después de los siguientes cambios generados con IA: una función nueva puede agregar fácilmente una tabla sin políticas. Una prueba automática corta que entra como B e intenta leer un registro de A atrapa estas regresiones para siempre.

¿Y si hubo usuarios reales afectados?

Primero ciérralo; después averigua hasta dónde llegó. Revisa los registros de la base de datos y de la API buscando peticiones que leyeron registros de otros usuarios durante el tiempo que existió la falla. Si se expusieron datos personales, puede que tengas obligaciones legales de reportarlo o avisar a las personas afectadas, según dónde estés tú y dónde estén ellas. Lee nuestro resumen en palabras simples sobre protección de datos para tu app y consulta con un abogado.

Respuestas rápidas

¿Basta con esconder el enlace en la interfaz? No. Cualquiera puede escribir una URL o repetir una petición. La verificación tiene que estar en el servidor o en la base de datos.

¿Los UUID evitan el IDOR? Hacen más difícil adivinar, pero no frenan a quien ya vio un ID. Igual necesitas verificar la pertenencia.

Activé RLS y ahora todo sale vacío. ¿Qué pasó? RLS sin políticas lo niega todo. Agrega políticas para las operaciones que necesita cada rol.

¿Esto aplica también a los archivos? Sí. Los buckets de almacenamiento necesitan sus propias políticas; un bucket público entrega cualquier archivo a quien tenga la ruta.

¿Quieres que lo revisemos a fondo?

El chequeo de salud gratis muestra lo que tu app revela desde afuera. Para que una persona recorra tabla por tabla tus reglas de base de datos y tus endpoints, nuestra revisión de seguridad de Supabase está pensada justo para esto, o puedes contarnos qué encontraste.

¿Tu app sigue trabada?

Pasa el chequeo gratis con la URL de tu app, o pide la Auditoría de rescate de $299: un ingeniero senior revisa el código y te envía un informe escrito en 2 días hábiles.