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

Las IDOR son la principal forma en que las empresas SaaS multi-inquilino filtran datos, y suelen descubrirse después del despliegue. Aikido Zen hace que el aislamiento de inquilinos no sea opcional. Zen analiza todas sus consultas SQL en tiempo de ejecución utilizando un analizador SQL adecuado (escrito en Rust), verifica que cada consulta filtre por el inquilino correcto y lanza un error si no lo hace. Los desarrolladores ya no pueden implementar accidentalmente el acceso entre inquilinos. Está disponible hoy para Node.js, y próximamente para Python, PHP, Go, Ruby, Java y .NET.

Por qué los IDORs son más peligrosos ahora

Las vulnerabilidades IDOR, también conocidas como Referencias Directas Inseguras a Objetos, son uno de los fallos más comunes y peligrosos en las aplicaciones multi-tenant. Ocurren cuando una consulta olvida filtrar por inquilino, permitiendo que una cuenta acceda a los datos de otra.

Durante mucho tiempo, los IDORs fueron difíciles de detectar. No aparecían en los escaneos de código y requerían un gran esfuerzo manual. Debido a ello, muchos fallos IDOR solo se descubrían durante pentests costosos y laboriosos, o cuando los investigadores de seguridad los encontraban a través de programas de recompensas por errores (bug bounty programs).

Pero eso ha cambiado. Las herramientas de pruebas de seguridad agenticas ahora pueden comportarse como usuarios reales, navegando por flujos de trabajo, cambiando 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 solo deben centrarse en detectar los IDORs, sino en prevenirlos.

Por qué la detección no es suficiente

En cuanto a la detección, el AI Pentest de Aikido ya puede encontrar vulnerabilidades IDOR, algo que las herramientas SAST tradicionales basadas en patrones nunca podrían hacer de forma fiable, porque IDOR requiere comprender el contexto de autorización, no solo patrones de código. AI Pentest se autentica como usuarios reales, ejecuta flujos de trabajo completos y reutiliza identificadores de objetos entre roles para encontrar fallos IDOR explotables reales. Es por esta razón que muchas organizaciones que utilizan nuestra capacidad de AI Pentest están predominantemente interesadas en encontrar IDORs.

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. Analiza cada consulta SQL en tiempo de ejecución y lanza un error si a una consulta le falta 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 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 multi-tenancy y cómo se evita el acceso a datos entre inquilinos.

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

Disponer de un mecanismo que valide automáticamente el alcance del inquilino a nivel de consulta proporciona una respuesta directa y creíble. Cambia la conversación de «confiamos en 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'");
});

¿Qué aspecto tiene una vulnerabilidad IDOR?

Si su aplicación tiene cuentas, organizaciones, espacios de trabajo o equipos, probablemente tenga una columna como tenant_id que mantiene los datos de cada cuenta separados. Pero cuando la consulta olvida filtrar por esa columna, o filtra por el valor incorrecto, significa que una cuenta puede acceder a los datos de otra. Esto es una vulnerabilidad IDOR.  

Aquí hay un ejemplo sencillo. Tiene un endpoint 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);
});

¿Ve el problema? No hay ningún tenant_id filtro. Si Alice envía GET /orders/42 y el pedido 42 pertenece a Bob, Alice obtiene el pedido de Bob. Eso es un IDOR.

La solución es sencilla, añada una cláusula WHERE tenant_id = $2 . 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 filtro omitido.

IDOR es una categoría amplia. También incluye aspectos como el acceso a archivos de otros usuarios mediante manipulación de URL o endpoints de API que no verifican 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 esté correctamente delimitada al inquilino actual. Para una inmersión más profunda en IDOR en un sentido más amplio, consulte nuestra publicación Vulnerabilidades IDOR explicadas.
Qué hay disponible hoy en día

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

Bibliotecas a nivel de framework

Varios frameworks tienen bibliotecas que delimitan automáticamente las consultas al inquilino actual:

  • Ruby on Rails: acts_as_tenant añade un alcance de inquilino automático a los modelos de ActiveRecord. Declare acts_as_tenant(:account) y todas las consultas sobre ese modelo se filtrarán por el inquilino actual.
  • Django: django-multitenant hace lo mismo para el ORM de Django. Establezca el inquilino actual en el middleware, y Product.objects.all() se delimitará automáticamente al inquilino.
  • Laravel: Tenancy for Laravel proporciona multi-tenancy tanto de base de datos única como de múltiples bases de datos, con cambio de contexto automático.
  • .NET / EF Core: Filtros de consulta globales permiten aplicar WHERE tenant_id = X a cada consulta automáticamente a nivel de modelo.

Estas librerías 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 puras, las consultas de otras librerías o las consultas construidas con un constructor de consultas diferente en el mismo proyecto no serán acotadas. Además, son opt-in; hay que recordar añadir la anotación a cada modelo, y los modelos nuevos pueden pasar desapercibidos. Para ser justos, acts_as_tenant tiene una require_tenant configuración que lanza un error cuando no se ha establecido un tenant, lo que mitiga significativamente el riesgo de "olvidar establecer el tenant".

También hay trampas sutiles. En Rails, por ejemplo, acts_as_tenant funciona añadiendo un default_scope. Si un desarrollador llama a Project.unscoped para eliminar un ámbito predeterminado diferente, como un filtro archived , elimina todos los ámbitos predeterminados, incluido el filtro de tenant, sin error ni advertencia. Rails tiene unscope (sin el d) para eliminar quirúrgicamente un único ámbito, pero eso requiere saber que el ámbito de tenant existe en primer lugar. En una base de código con muchos desarrolladores, alguien acabará recurriendo a unscoped, y el límite del tenant desaparece silenciosamente.

Aplicación a nivel de base de datos

PostgreSQL Row-Level Security (RLS) lleva esto un paso más allá al aplicar el aislamiento de inquilinos a nivel de base de datos. En lugar de depender de que su aplicación añada WHERE tenant_id = ? a cada consulta, le dice al propio Postgres que lo aplique:

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

-- 2. Crear una política: solo permitir filas que coincidan con la variable de sesión
CREATE POLICY tenant_isolation ON projects
  FOR ALL
  USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

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

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

RLS es la garantía más sólida de los enfoques aquí enumerados. Consultas SQL puras o de ORM, no importa. La base de datos lo aplica. Y a diferencia de acts_as_tenant, si olvida establecer la variable de sesión, no se devuelven datos en lugar de todos los datos. Ese es un valor predeterminado mucho más seguro.

Pero conlleva compensaciones reales. RLS no lanza 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 UPDATE que debería modificar 100 filas podría afectar silenciosamente a 0 debido a un desajuste de política, y es difícil distinguir esto de "no existen datos".

El pool de conexiones también añade complejidad. RLS con SET no funciona correctamente con pgBouncer en modo de pool de sentencias o transacciones, lo que puede llevar a que se devuelvan filas para el inquilino equivocado. Esto solo puede manifestarse en producción.

También existen limitaciones estructurales. Los superusuarios eluden todas las políticas por completo, y las vistas eluden RLS por defecto, por lo que su aplicación debe conectarse como un rol que no sea de superusuario. Finalmente, es solo para Postgres. Si necesita soportar MySQL, SQLite para desarrollo u otro almacén de datos, su capa de seguridad no le acompaña.

La 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 significan que no es una solución lista para usar para todos los equipos.

Dónde encaja Zen

Todos estos son enfoques válidos, y si está utilizando uno de ellos, ¡genial! La protección IDOR de Zen está diseñada para un escenario diferente: sus consultas pasan por un controlador de base de datos, directamente o a través de un ORM, y usted desea una red de seguridad que funcione independientemente del ORM, constructor de consultas o patrón SQL puro que utilice, sin cambiar la configuración de su base de datos ni adoptar una biblioteca de framework específica.

Zen tiene sus propias compensaciones, para ser honestos. Al igual que acts_as_tenant, requiere que llame a setTenantId en cada solicitud. Si lo olvida, Zen lanza un error, por lo que falla de forma ruidosa en lugar de silenciosa, pero es la misma clase 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 directamente a la base de datos, por ejemplo a través de psql o un servicio separado sin Zen, esas consultas no se verifican.

También es agnóstico al lenguaje por diseño. Debido a que el motor de análisis SQL está escrito en Rust, lo compilamos a WebAssembly para Node.js y Go, y una biblioteca nativa que otros agentes llaman a través de FFI. La protección IDOR específicamente también llegará a los agentes de Python, PHP, Go, Ruby, Java y .NET.

Cómo Zen protege contra los IDOR

Zen se sitúa dentro de su aplicación y analiza las consultas SQL en tiempo de ejecución, con el contexto completo sobre quién realiza la solicitud.

Un analizador SQL adecuado, escrito en Rust

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

  • Qué tablas afecta la consulta (incluyendo alias)
  • Qué filtros de igualdad se encuentran en la cláusula WHERE
  • Qué columnas y valores se encuentran en las sentencias INSERT

¿Por qué no regex? Las expresiones regulares funcionan bien para consultas sencillas como SELECT * FROM orders WHERE tenant_id = ?. Pero las aplicaciones del mundo real tienen CTEs, UNIONs, subconsultas, JOINs con alias y todo tipo de SQL válido con el que un enfoque basado en regex tiene dificultades. A medida que las consultas se vuelven más complejas, el análisis basado en regex se vuelve cada vez más frágil. No es necesariamente incorrecto, pero es difícil de mantener y fácil de que te sorprenda.

Un analizador adecuado maneja todo esto de forma nativa. También reconoce correctamente las sentencias que no necesitan verificación, como las sentencias DDL (CREATE TABLE, ALTER TABLE), control de transacciones (BEGIN, COMMIT, ROLLBACK) y comandos de sesión (SET, SHOW).

Así es como se ve el análisis internamente. Dada esta consulta:

SELECT * FROM orders
LEFT JOIN order_items ON orders.id = order_items.order_id
WHERE orders.tenant_id = $1
AND orders.status = 'active';

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" }
    ]
  }
]

Zen verifica entonces si cada tabla en la consulta tiene un filtro en tenant_id, y si el valor del filtro coincide con el inquilino actual.

Lo mismo se aplica a INSERT, UPDATE, y DELETE. Zen se asegura de que la columna de inquilino (tenant) esté siempre presente y tenga siempre el valor correcto. Estos se lanzan como errores, no solo se registran. IDOR es un error de desarrollador, no un ataque externo, por lo que se desea que se manifieste ruidosamente durante el desarrollo y las pruebas en lugar de pasar desapercibido hasta la producción.

Rendimiento

Analizar SQL en cada consulta suena costoso, pero en la práctica es rápido. La clave es 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. Así que SELECT * FROM orders WHERE tenant_id = $1 AND status = $2 se analiza una vez, y cada ejecución posterior de esa misma consulta es un acierto de caché.

La primera vez que Zen detecta una nueva cadena de consulta, el analizador de Rust construye el AST y extrae las tablas y los filtros. A partir de entonces, solo se realiza una búsqueda en caché y una comparación del ID de inquilino con el valor de marcador de posición resuelto.

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

El camino a producción: aplicando lo que predicamos

Implementamos la protección IDOR de Zen en varios de los servicios internos de Aikido. Esto reveló inmediatamente casos extremos que requerían ser gestionados.

El soporte de transacciones fue un obstáculo al principio. Las aplicaciones reales utilizan BEGIN, COMMIT, y ROLLBACK, y Zen necesitaba reconocer estas como sentencias seguras que no requieren filtrado por inquilino, en lugar de generar errores. Añadimos esto rápidamente después de ver que fallaba en nuestra primera implementación interna.

Las Common Table Expressions (CTEs) fueron otro desafío. Una CTE como WITH active AS (SELECT * FROM orders WHERE 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”, mientras seguía analizando el cuerpo de la CTE para un filtrado adecuado.

El withoutIdorProtection El Escape también resultó esencial. No todas las consultas necesitan filtrado por inquilino, como los paneles de administración, los trabajos en segundo plano o los análisis entre inquilinos. Inicialmente probamos un enfoque de ignoreNextQuery enfoque, donde se llamaría a una función antes de la consulta para omitir la comprobación de la siguiente sentencia SQL:

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

Esto resultó ser frágil en la práctica. Con los pools de conexiones, la "siguiente consulta" en una conexión dada podría no ser la que se pretendía omitir. El enfoque basado en callback withoutIdorProtection es explícito sobre el alcance. La protección IDOR se desactiva durante la duración del callback y nada más.

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

Uno de los servicios que protegimos al principio fue la API interna que sirve datos de activos en la nube en toda la plataforma.

Esta API es utilizada por la UI, los trabajos en segundo plano y varios motores de seguridad siempre 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 completamente multi-inquilino, el aislamiento estricto de inquilinos es crítico. Cada consulta debe estar delimitada a la organización correcta, y no podemos depender de que los desarrolladores recuerden añadir el filtro adecuado en cada ruta de código.

Antes de que Zen soportara la protección IDOR de forma nativa, teníamos una implementación personalizada que aplicaba el alcance de inquilino a nivel de consulta. Una vez que Zen introdujo soporte de primera clase para este comportamiento, migramos de la solución propia a la funcionalidad integrada, lo que supone considerablemente menos código para nosotros.

Hoy en día, Zen verifica automáticamente que las consultas estén correctamente delimitadas al inquilino actual, incluso bajo una carga pesada. Tras introducir la protección IDOR de Zen, no observamos un impacto notable en el rendimiento.

Detección y prevención

El AI Pentest de Aikido encuentra vulnerabilidades IDOR en su aplicación en ejecución simulando ataques reales. La protección IDOR de Zen evita que se introduzcan en primer lugar al detectar filtros de inquilino ausentes durante el desarrollo.

Juntos, cubren ambos lados. AI Pentest valida que su código existente es seguro, y Zen asegura que el código nuevo se mantenga seguro. Utilice AI Pentest para auditar lo que ya está desplegado. Utilice Zen para detectar errores mientras los escribe.

Primeros pasos

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

Compartir:

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

Suscríbase para recibir noticias sobre amenazas.

4.7/5
¿Cansado de los falsos positivos?

Prueba Aikido como otros 100k.
Empiece ahora
Obtenga un recorrido personalizado

Con la confianza de más de 100k equipos

Reservar ahora
Escanee su aplicación en busca de IDORs y rutas de ataque reales

Con la confianza de más de 100k equipos

Empezar a escanear
Vea cómo el pentesting de IA prueba su aplicación

Con la confianza de más de 100k equipos

Empezar a probar

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.