Aikido

De la detección a la prevención: cómo Zen detiene las vulnerabilidades IDOR en tiempo de ejecución

Escrito por
Hans Ott

TL;DR

Los IDOR son la principal causa de fuga de datos en las empresas SaaS multitenant, y suelen descubrirse tras la implementación. Aikido Zen hace que el aislamiento de los tenants sea obligatorio. Zen analiza todas las consultas SQL en tiempo de ejecución utilizando un analizador SQL adecuado (escrito en Rust), comprueba que cada consulta se filtre en el tenant correcto y genera un error si no es así. Los desarrolladores ya no pueden enviar accidentalmente accesos entre tenants. Ya está disponible para Node.js, y pronto lo estará para Python, PHP, Go, Ruby, Java y .NET.

Por qué los IDOR son ahora más peligrosos

Las vulnerabilidades IDOR, también conocidas como referencias directas a objetos inseguras, son uno de los fallos más comunes y peligrosos en las aplicaciones multitenant. Se producen cuando una consulta olvida filtrar por inquilino, lo que permite que una cuenta acceda a los datos de otra cuenta.

Durante mucho tiempo, los IDOR eran difíciles de detectar. No aparecían en los análisis de código y requerían mucho esfuerzo manual. Por eso, muchos errores IDOR solo se descubrían durante costosas y laboriosas pruebas de penetración, o cuando los investigadores de seguridad los encontraban a través de programas de recompensa por errores.

Pero eso ha cambiado. Las herramientas de pruebas de seguridad agenticas ahora pueden comportarse como usuarios reales, haciendo clic en los flujos de trabajo, cambiando de roles e intentando acceder automáticamente a los recursos. Esto hace que las vulnerabilidades IDOR sean mucho más fáciles de detectar. Pero esto es un arma de doble filo: si estos fallos son más fáciles de encontrar, también son más fáciles de explotar. Por eso las organizaciones no deben centrarse solo en detectar los IDOR, sino también en prevenirlos.

Por qué la detección no es suficiente

En cuanto a la detección, AI Pentest de Aikido ya puede encontrar vulnerabilidades IDOR, algo que SAST tradicionales basadas en patrones nunca podrían hacer de forma fiable, ya que IDOR requiere comprender el contexto de autorización, no solo los patrones de código. AI Pentest se autentica como usuarios reales, ejecuta flujos de trabajo completos y reutiliza identificadores de objetos en todas las funciones para encontrar fallos IDOR realmente explotables. Por este motivo, muchas organizaciones que utilizan nuestra capacidad AI Pentest están principalmente interesadas en encontrar IDOR.

Pero encontrar vulnerabilidades IDOR es solo la mitad de la ecuación. En un mundo ideal, se evitaría que se introdujeran en primer lugar. De eso trata esta publicación: la protección IDOR en Zen, nuestro firewall integrado en la aplicación de código abierto firewall integrado en la aplicación. Analiza cada consulta SQL en tiempo de ejecución y genera un error si una consulta carece de un filtro de inquilino o utiliza un ID de inquilino incorrecto, detectando el error durante el desarrollo y las pruebas antes de que llegue a la producción.

En muchos entornos empresariales, especialmente durante las revisiones de seguridad o las evaluaciones de proveedores, una pregunta recurrente es cómo se aplica la multitenencia y cómo se evita el acceso a datos entre tenants.

Los equipos de seguridad y los directivos quieren garantías técnicas claras de que los límites de los inquilinos se aplican de forma sistemática, y no solo por convención.

Contar con un mecanismo que valida automáticamente el alcance de los inquilinos a nivel de consulta proporciona una respuesta sencilla y creíble. De este modo, la conversación pasa de «dependemos de que los desarrolladores lo recuerden» a «el sistema lo aplica automáticamente».

Así es como se ve la configuración:

import Zen from "@aikidosec/firewall";

// 1. Tell Zen which column identifies the tenant
Zen.enableIdorProtection({
  tenantColumnName: "tenant_id",
  excludedTables: ["users"],
});

// 2. Set the tenant ID per request (e.g., in middleware after authentication)
app.use((req, res, next) => {
  Zen.setTenantId(req.user.organizationId);
  next();
});

// 3. Optionally bypass for specific queries (e.g., admin dashboards)
const result = await Zen.withoutIdorProtection(async () => {
  return await db.query("SELECT count(*) FROM orders WHERE status = 'active'");
});

¿Cómo es una vulnerabilidad IDOR?

Si tu aplicación tiene cuentas, organizaciones, espacios de trabajo o equipos, probablemente tengas una columna como identificador_del_inquilino que mantiene separados los datos de cada cuenta. Pero cuando la consulta olvida filtrar esa columna, o filtra con un valor incorrecto, una cuenta puede acceder a los datos de otra. Se trata de una vulnerabilidad IDOR.  

Aquí hay un ejemplo sencillo. Tienes un punto final que devuelve los pedidos de un usuario:

app.get("/orders/:orderId", async (req, res) => {
  const order = await db.query(
    "SELECT * FROM orders WHERE id = $1",
    [req.params.orderId]
  );

  res.json(order);
});

¿Ves el problema? No hay identificador_del_inquilino filtro. Si Alice envía GET /pedidos/42 y el pedido 42 pertenece a Bob, Alice recibe el pedido de Bob. Eso es un IDOR.

La solución es sencilla, añade un DONDE tenant_id = $2 cláusula. Sin embargo, el error es fácil de introducir y difícil de detectar, especialmente en una base de código grande con cientos de consultas en docenas de archivos. Basta con un solo filtro omitido.

IDOR es una categoría amplia. También incluye cosas como acceder a los archivos de otros usuarios mediante la manipulación de URL o puntos finales de API que no comprueban la propiedad. Esta publicación se centra específicamente en el subconjunto de filtrado de inquilinos SQL, asegurándose de que cada consulta de base de datos tenga el alcance adecuado para el inquilino actual. Para profundizar más en IDOR de manera más amplia, consulte nuestra publicación sobre vulnerabilidades IDOR explicadas.
¿Qué hay hoy en día?

Aparte del análisis de seguridad, que se centra más en encontrar errores existentes, existen otros métodos para evitar que se introduzcan nuevos IDOR: bibliotecas a nivel de marco y aplicación a nivel de base de datos. Cada uno tiene sus puntos fuertes y sus limitaciones.

Bibliotecas a nivel de marco

Varios marcos tienen bibliotecas que limitan automáticamente las consultas al inquilino actual:

  • Ruby on Rails: acts_as_tenant añade un ámbito de inquilino automático a los modelos ActiveRecord. Declare acts_as_tenant(:account) y todas las consultas en ese modelo se filtrarán por el inquilino actual.
  • Django: django-multitenant hace lo mismo para el ORM de Django. Establece el inquilino actual en el middleware y Product.objects.all() automáticamente pasa a tener el ámbito del inquilino.
  • Laravel: Tenancy para Laravel ofrece tanto multitenencia de una sola base de datos como multitenencia de múltiples bases de datos, con cambio automático de contexto.
  • .NET / EF Core: Filtros de consulta globales te permite aplicar DONDE tenant_id = X a cada consulta automáticamente a nivel de modelo.

Estas bibliotecas funcionan bien dentro de su propio ORM. La limitación es que solo protegen las consultas que pasan por la abstracción del ORM. Las consultas SQL sin procesar, las consultas de otras bibliotecas o las consultas creadas con un generador de consultas diferente en el mismo proyecto no estarán incluidas. También son opcionales, hay que recordar añadir la anotación a cada modelo, y los nuevos modelos pueden pasar desapercibidos sin que nadie se dé cuenta. Para ser justos, actúa_como_inquilino tiene un requerir_inquilino Configuración que genera un error cuando no se ha establecido ningún inquilino, lo que mitiga significativamente el riesgo de «olvidar establecer el inquilino».

También hay footguns sutiles. En Rails, por ejemplo, actúa_como_inquilino funciona añadiendo un alcance predeterminado. Si un desarrollador llama Proyecto sin alcance definido para eliminar un ámbito predeterminado diferente, como un archivado filtro, elimina todos los ámbitos predeterminados, incluido el filtro de inquilino, sin mostrar ningún error ni advertencia. Rails tiene desactivar (sin el d) para eliminar quirúrgicamente un único ámbito, pero eso requiere saber que el ámbito del inquilino existe en primer lugar. En un código base con muchos desarrolladores, alguien acabará recurriendo a sin ámbito, y el límite del inquilino desaparece silenciosamente.

Aplicación a nivel de base de datos

Seguridad a nivel de fila (RLS) de PostgreSQL va un paso más allá al aplicar el aislamiento de inquilinos a nivel de base de datos. En lugar de depender de su aplicación para añadir ¿DÓNDE tenant_id = ? Para cada consulta, le indicas a Postgres que la aplique:

-- 1. Habilitar RLS en cada tabla
ALTER TABLE proyectos ENABLE ROW LEVEL SECURITY;

-- 2. Crear una política: solo permitir filas que coincidan con la variable de sesión
CREAR POLICY tenant_isolation EN proyectos
  FOR TODOS
  USANDO (tenant_id = configuración_actual('app.current_tenant_id')::uuid);

-- 3. Por solicitud, establecer el contexto del inquilino antes de ejecutar consultas
SET app.current_tenant_id = 'aaaa-aaaa-aaaa';

-- Ahora, incluso un simple SELECT solo devuelve las filas de ese inquilino
SELECT * FROM proyectos;

RLS es la garantía más sólida de los enfoques enumerados aquí. No importa si se trata de consultas SQL sin procesar u ORM. La base de datos lo impone. Y, a diferencia de actúa_como_inquilinoSi olvida establecer la variable de sesión, no se devolverá ningún dato en lugar de todos los datos. Esa es una opción predeterminada mucho más segura.

Pero esto conlleva verdaderas desventajas. RLS no genera errores; las consultas devuelven silenciosamente menos filas o no afectan a nada. Esto es más seguro que devolver todos los datos, pero dificulta la depuración. Un ACTUALIZACIÓN que debería modificar 100 filas podría afectar silenciosamente a 0 debido a una incompatibilidad de políticas, y es difícil distinguir esto de «no existen datos».

El agrupamiento de conexiones también añade complejidad. RLS con CONJUNTO No funciona correctamente con pgBouncer en modo de agrupación de sentencias o transacciones, lo que puede provocar que se devuelvan filas para el inquilino incorrecto. Esto solo puede aparecer en producción.

También existen limitaciones estructurales. Los superusuarios eluden todas las políticas por completo y las vistas eluden RLS de forma predeterminada, por lo que su aplicación debe conectarse como un rol que no sea de superusuario. Por último, solo es compatible con Postgres. Si necesita compatibilidad con MySQL, SQLite para desarrollo u otro almacén de datos, su capa de seguridad no le acompañará.

Conclusión pragmática: RLS es excelente como red de seguridad para el aislamiento de inquilinos, pero la complejidad operativa y la dificultad de depuración hacen que no sea una solución válida para todos los equipos.

Dónde encaja el Zen

Todos estos enfoques son válidos, y si estás utilizando alguno de ellos, estupendo. La protección IDOR de Zen está diseñada para un escenario diferente: tus consultas pasan por un controlador de base de datos, directamente o a través de un ORM, y quieres una red de seguridad que funcione independientemente del ORM, el generador de consultas o el patrón SQL sin procesar que utilices, sin cambiar la configuración de tu base de datos ni adoptar una biblioteca de marco específica.

El zen tiene sus propias ventajas e inconvenientes, para ser sinceros. Como actúa_como_inquilino, requiere que llames establecerIdInquilino en cada solicitud. Si se olvida, Zen genera un error, por lo que falla de forma evidente en lugar de silenciosa, pero es el mismo tipo de configuración por solicitud. Y, a diferencia de RLS, Zen solo cubre las consultas que se ejecutan dentro de su aplicación. Si alguien se conecta a la base de datos directamente, por ejemplo, a través de psql o un servicio independiente sin Zen, esas consultas no se comprueban.

Además, su diseño es independiente del lenguaje. Dado que el motor de análisis SQL está escrito en Rust, lo compilamos en WebAssembly para Node.js y Go, y en una biblioteca nativa que otros agentes invocan a través de FFI. La protección IDOR también estará disponible para los agentes Python, PHP, Go, Ruby, Java y .NET.

Cómo protege Zen contra los IDOR

Zen se integra en tu aplicación y analiza las consultas SQL en tiempo de ejecución, con información completa sobre quién realiza la solicitud.

Un analizador sintáctico SQL adecuado, escrito en Rust.

En el corazón de la protección IDOR de Zen se encuentra un analizador SQL real basado en el crate sqlparser de Rust, compilado en WebAssembly para Node.js y Go. Analiza el SQL de la misma manera que lo haría una base de datos, construyendo un árbol sintáctico abstracto (AST) completo de la consulta y, a continuación, recorre el árbol para extraer:

  • ¿Qué tablas afecta la consulta (incluidos los alias)?
  • ¿Qué filtros de igualdad hay en la cláusula WHERE?
  • ¿Qué columnas y valores hay en las sentencias INSERT?

¿Por qué no usar expresiones regulares? Las expresiones regulares funcionan bien para consultas simples como SELECT * FROM pedidos WHERE id_inquilino = ?. Pero las aplicaciones del mundo real tienen CTE, UNION, subconsultas, JOIN con alias y todo tipo de SQL válido con el que un enfoque basado en expresiones regulares tiene problemas. A medida que las consultas se vuelven más complejas, el análisis basado en expresiones regulares se vuelve cada vez más frágil. No es necesariamente incorrecto, pero es difícil de mantener y fácil de sorprender.

Un analizador sintáctico adecuado gestiona todo esto de forma inmediata. También reconoce correctamente las sentencias que no necesitan comprobación, como las sentencias DDL (CREAR TABLA, MODIFICAR TABLA), control de transacciones (BEGIN, COMMIT, ROLLBACK) y comandos de sesión (CONFIGURAR, MOSTRAR).

Así es como se ve el análisis bajo el capó. Dada esta consulta:

SELECCIONAR * FROM pedidos
IZQUIERDA UNIÓN elementos_del_pedido EN pedidos.id = artículos_pedido.id_pedido
WHERE pedidos.tenant_id = $1
Y estado de los pedidos = 'activo';

El analizador produce:

[
  {
    "kind": "select",
    "tables": [
      { "name": "orders" },
      { "name": "order_items" }
    ],
    "filters": [
      { "table": "orders", "column": "tenant_id", "value": "$1" },
      { "table": "orders", "column": "status", "value": "active" }
    ]
  }
]

A continuación, Zen comprueba si todas las tablas de la consulta tienen un filtro activado. identificador_del_inquilino, y si el valor del filtro coincide con el inquilino actual.

Lo mismo se aplica a INSERTAR, ACTUALIZACIÓN, y ELIMINARZen se asegura de que la columna «tenant» esté siempre presente y tenga siempre el valor correcto. Estos se consideran errores, no solo se registran. IDOR es un error de desarrollo, no un ataque externo, por lo que es preferible que se detecte durante el desarrollo y las pruebas, en lugar de pasar desapercibido hasta la fase de producción.

Rendimiento

Analizar SQL en cada consulta parece costoso, pero en la práctica es rápido. La clave está en que la mayoría de las aplicaciones utilizan sentencias preparadas o consultas parametrizadas. La cadena SQL permanece igual, solo cambian los valores de los parámetros. Por lo tanto, SELECT * FROM pedidos WHERE id_inquilino = $1 AND estado = $2 Se analiza una vez, y cada ejecución posterior de esa misma consulta es un acierto de caché.

La primera vez que Zen ve una nueva cadena de consulta, el analizador Rust construye el AST y extrae las tablas y los filtros. Cada vez después de eso, solo se trata de una búsqueda en la caché y una comparación del ID del inquilino con el valor del marcador de posición resuelto.

Si está incrustando valores directamente en cadenas SQL, por ejemplo, concatenación de cadenas en lugar de consultas parametrizadas, cada cadena única requiere un nuevo análisis. Pero probablemente no debería hacerlo de todos modos. Las consultas parametrizadas protegen contra la inyección SQL y también agilizan la comprobación IDOR.

El camino hacia la producción: probando nuestro propio producto

Implementamos la protección IDOR de Zen en varios de los servicios internos de Aikido. Esto sacó a la luz inmediatamente casos extremos que debían tratarse.

El soporte de transacciones fue un obstáculo al principio. Las aplicaciones reales utilizan COMIENZO, COMPROMETERSE, y ROLLBACK, y Zen necesitaba reconocerlas como declaraciones seguras que no requieren filtrado de inquilinos, en lugar de considerarlas como errores. Añadimos esto rápidamente después de ver que fallaba en nuestra primera implementación interna.

Las expresiones de tabla comunes (CTE) fueron otro reto. Una CTE como CON activo AS (SELECCIONAR * DE pedidos DONDE tenant_id = $1) crea una tabla virtual a la que hacen referencia las consultas posteriores. Zen necesitaba rastrear los nombres de las CTE y excluirlos de la lista de «tablas reales», sin dejar de analizar el cuerpo de las CTE para aplicar el filtrado adecuado.

El sin protección contra la idolatría escape también resultó esencial. No todas las consultas necesitan filtrado de inquilinos, como los paneles de control de administración, los trabajos en segundo plano o los análisis entre inquilinos. Inicialmente probamos un ignorarSiguienteConsulta Enfoque en el que se llamaría a una función antes de la consulta para omitir la comprobación de la siguiente instrucción SQL:

Zen.ignoreNextQuery();
const resultado = await db.query("SELECT count(*) FROM orders");

En la práctica, esto resultó ser frágil. Con los grupos de conexiones, la «siguiente consulta» en una conexión determinada podría no ser la que se pretendía omitir. La basada en devolución de llamada sin protección contra la idolatría es explícito en cuanto al alcance. La protección IDOR se desactiva durante la duración de la devolución de llamada y nada más.

Cómo protegimos nuestra API que proporciona datos de activos en la nube

Uno de los servicios que protegimos desde el principio fue la API interna que proporciona datos de activos en la nube en toda la plataforma.

Esta API es utilizada por la interfaz de usuario, los trabajos en segundo plano y varios motores de seguridad cada vez que necesitan leer información sobre la infraestructura de un cliente. Se encuentra en el núcleo del sistema y gestiona miles de solicitudes por segundo.

Dado que la plataforma es totalmente multitenant, es fundamental que exista un aislamiento estricto entre los distintos usuarios. Cada consulta debe limitarse a la organización correcta, y no podemos confiar en que los desarrolladores recuerden añadir el filtro adecuado en cada ruta de código.

Antes de que Zen admitiera la protección IDOR de forma nativa, teníamos una implementación personalizada que aplicaba el alcance de los inquilinos a nivel de consulta. Una vez que Zen introdujo la compatibilidad de primera clase para este comportamiento, migramos de la solución propia a la funcionalidad integrada, lo que nos supone una reducción considerable del código que debemos mantener.

Hoy en día, Zen verifica automáticamente que las consultas se ajusten correctamente al ámbito del inquilino actual, incluso bajo una carga pesada. Tras introducir la protección IDOR de Zen, no observamos ningún impacto notable en el rendimiento.

Detección y prevención

La prueba de penetración de IA de Aikido detecta vulnerabilidades IDOR en su aplicación en ejecución mediante la simulación de ataques reales. La protección IDOR de Zen evita que se introduzcan en primer lugar, detectando los filtros de inquilinos que faltan en el momento del desarrollo.

Juntos, cubren ambos aspectos. AI Pentest valida que tu código existente es seguro, y Zen garantiza que el nuevo código siga siendo seguro. Utiliza AI Pentest para auditar lo que ya está implementado. Utiliza Zen para detectar errores a medida que los escribes.

Primeros pasos

La protección IDOR ya está disponible en @aikidosec/firewall para Node.js. Consulte la guía de configuración para empezar. ¡Próximamente estará disponible la compatibilidad con otros lenguajes!

Compartir:

https://www.aikido.dev/blog/zen-stops-idor-vulnerabilities

Suscríbase para recibir noticias sobre amenazas.

Asegura tu plataforma ahora

Protege tu código, la nube y el entorno de ejecución en un único sistema central.
Encuentra y corrije vulnerabilidades de forma rápida y automática.

No se requiere tarjeta de crédito | Resultados del escaneo en 32 segundos.