Aikido

De la détection à la prévention : Comment Zen stoppe les vulnérabilités IDOR à l'exécution

Écrit par
Hans Ott

TL;DR

Les IDOR sont la principale cause de fuites de données pour les entreprises SaaS multi-locataires, et elles sont généralement découvertes après le déploiement. Aikido Zen rend l'isolation des locataires obligatoire. Zen analyse toutes vos requêtes SQL à l'exécution à l'aide d'un parseur SQL approprié (écrit en Rust), vérifie que chaque requête filtre sur le bon locataire et génère une erreur si ce n'est pas le cas. Les développeurs ne peuvent plus introduire par inadvertance des accès inter-locataires. Il est disponible dès aujourd'hui pour Node.js, et sera bientôt disponible pour Python, PHP, Go, Ruby, Java et .NET.

Pourquoi les IDORs sont plus dangereuses maintenant

Les vulnérabilités IDOR, également connues sous le nom de Références d'Objet Directes Non Sécurisées, sont l'une des failles les plus courantes et dangereuses dans les applications multi-locataires. Elles surviennent lorsqu'une requête oublie de filtrer par locataire, permettant à un compte d'accéder aux données d'un autre compte.

Pendant longtemps, les IDORs étaient difficiles à détecter. Elles n'apparaissaient pas dans les analyses de code et nécessitaient beaucoup d'efforts manuels. De ce fait, de nombreux bugs IDOR n'étaient découverts que lors de pentests coûteux et laborieux, ou lorsque des chercheurs en sécurité les trouvaient via des programmes de bug bounty.

Mais cela a changé. Les outils de test de sécurité agentiques peuvent désormais se comporter comme de vrais utilisateurs, naviguant dans les workflows, changeant de rôle et tentant automatiquement d'accéder à des ressources. Cela rend les vulnérabilités IDOR beaucoup plus faciles à détecter. Mais c'est une arme à double tranchant : si ces failles sont plus faciles à trouver, elles sont aussi plus faciles à exploiter. C'est pourquoi les organisations ne devraient pas seulement se concentrer sur la détection des IDORs, mais aussi sur leur prévention.

Pourquoi la détection ne suffit pas.

Côté détection, l'AI Pentest d'Aikido peut déjà trouver des vulnérabilités IDOR, ce que les outils SAST traditionnels basés sur des motifs n'ont jamais pu faire de manière fiable, car les IDOR nécessitent une compréhension du contexte d'autorisation, et non pas seulement des motifs de code. AI Pentest s'authentifie comme de vrais utilisateurs, exécute des workflows complets et réutilise les identifiants d'objets entre les rôles pour trouver des failles IDOR réellement exploitables. C'est pour cette raison que de nombreuses organisations qui utilisent notre capacité AI Pentest sont principalement intéressées par la détection des IDOR.

Mais trouver les vulnérabilités IDOR n'est que la moitié de l'équation. Dans un monde idéal, vous les empêcheriez d'être introduites dès le départ. C'est l'objet de cet article : la protection IDOR dans Zen, notre pare-feu intégré à l’application open-source. Il analyse chaque requête SQL à l'exécution et génère une erreur si une requête manque un filtre de tenant ou utilise le mauvais ID de tenant, détectant ainsi le bug pendant le développement et les tests avant qu'il n'atteigne la production.

Dans de nombreux environnements d'entreprise, en particulier lors des audits de sécurité ou des évaluations de fournisseurs, une question récurrente est de savoir comment la multi-location est appliquée et comment l'accès aux données inter-locataires est empêché.

Les équipes de sécurité et la direction souhaitent des assurances techniques claires que les limites des locataires sont appliquées systématiquement, et non pas seulement par convention.

Disposer d'un mécanisme qui valide automatiquement la portée des locataires au niveau de la requête fournit une réponse simple et crédible. Cela déplace la conversation de « nous comptons sur les développeurs pour s'en souvenir » à « le système l'applique automatiquement ».

Voici à quoi ressemble la configuration :

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'");
});

À quoi ressemble une vulnérabilité IDOR ?

Si votre application gère des comptes, des organisations, des espaces de travail ou des équipes, vous avez probablement une colonne comme tenant_id qui maintient les données de chaque compte séparées. Mais lorsque la requête oublie de filtrer sur cette colonne, ou filtre sur la mauvaise valeur, cela signifie qu'un compte peut accéder aux données d'un autre. C'est une vulnérabilité IDOR.  

Voici un exemple simple. Vous avez un endpoint qui renvoie les commandes d'un utilisateur :

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

  res.json(order);
});

Voyez-vous le problème ? Il n'y a pas de tenant_id filtre. Si Alice envoie GET /orders/42 et que la commande 42 appartient à Bob, Alice obtient la commande de Bob. C'est un IDOR.

La correction est simple : ajoutez une clause WHERE tenant_id = $2 clause. Cependant, le bug est facile à introduire et difficile à repérer, surtout dans une grande base de code avec des centaines de requêtes réparties sur des dizaines de fichiers. Un seul filtre manquant suffit.

L'IDOR est une catégorie large. Elle inclut également des éléments tels que l'accès aux fichiers d'autres utilisateurs via la manipulation d'URL ou des endpoints d'API qui ne vérifient pas la propriété. Cet article se concentre spécifiquement sur le sous-ensemble de filtrage des locataires SQL, en s'assurant que chaque requête de base de données est correctement limitée au locataire actuel. Pour une exploration plus approfondie des IDOR en général, consultez notre article vulnérabilités IDOR expliquées.
Ce qui existe aujourd'hui

Outre l'analyse de sécurité, qui vise davantage à trouver les bugs existants, il existe d'autres méthodes pour empêcher l'introduction de nouveaux IDOR : les bibliothèques au niveau du framework et l'application au niveau de la base de données. Chacune a ses forces et ses limites.

Bibliothèques au niveau du framework

Plusieurs frameworks disposent de bibliothèques qui limitent automatiquement la portée des requêtes au tenant actuel :

  • Ruby on Rails: acts_as_tenant ajoute une portée automatique au tenant pour les modèles ActiveRecord. Déclarez acts_as_tenant(:account) et toutes les requêtes sur ce modèle seront filtrées par le tenant actuel.
  • Django: django-multitenant fait de même pour l'ORM de Django. Définissez le tenant actuel dans le middleware, et Product.objects.all() devient automatiquement limité à la portée du tenant.
  • Laravel: Tenancy for Laravel offre une architecture multi-tenant, à la fois en base de données unique et multi-bases de données, avec une commutation de contexte automatique.
  • .NET / EF Core: Filtres de requête globaux vous permettent d'appliquer WHERE tenant_id = X automatiquement à chaque requête au niveau du modèle.

Ces bibliothèques fonctionnent bien au sein de leur propre ORM. La limitation est qu'elles ne protègent que les requêtes passant par l'abstraction de l'ORM. Les requêtes SQL brutes, les requêtes provenant d'autres bibliothèques ou celles construites avec un constructeur de requêtes différent dans le même projet ne seront pas limitées en portée. Elles sont également opt-in : vous devez vous souvenir d'ajouter l'annotation à chaque modèle, et de nouveaux modèles peuvent passer inaperçus. Pour être juste, acts_as_tenant dispose d'un require_tenant config qui lève une erreur si aucun tenant n'est défini, ce qui atténue considérablement le risque d'« oubli de définition du tenant ».

Il existe également des pièges subtils. Dans Rails, par exemple, acts_as_tenant fonctionne en ajoutant un default_scope. Si un développeur appelle Project.unscoped pour supprimer une autre portée par défaut, comme un filtre archivé archivé, cela supprime toutes les portées par défaut, y compris le filtre de tenant, sans erreur ni avertissement. Rails dispose de unscope (sans le d) pour supprimer chirurgicalement une portée unique, mais cela nécessite de savoir que la portée du tenant existe en premier lieu. Dans une base de code avec de nombreux développeurs, quelqu'un finira par utiliser unscoped, et la limite du tenant disparaît silencieusement.

Application au niveau de la base de données

PostgreSQL Row-Level Security (RLS) va plus loin en appliquant l'isolation des tenants au niveau de la base de données. Au lieu de vous fier à votre application pour ajouter WHERE tenant_id = ? à chaque requête, vous indiquez à Postgres lui-même de l'appliquer :

-- 1. Activer RLS sur chaque table
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;

-- 2. Créer une politique : n'autoriser que les lignes correspondant à la variable de session
CREATE POLICY tenant_isolation ON projects
  FOR ALL
  USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

-- 3. Par requête, définir le contexte du tenant avant d'exécuter les requêtes
SET app.current_tenant_id = 'aaaa-aaaa-aaaa';

-- Maintenant, même un simple SELECT ne renvoie que les lignes de ce tenant
SELECT * FROM projects;

RLS est la garantie la plus solide parmi les approches listées ici. Requêtes SQL brutes ou ORM, peu importe. La base de données l'applique. Et contrairement à acts_as_tenant, si vous oubliez de définir la variable de session, aucune donnée n'est renvoyée plutôt que toutes les données. C'est un comportement par défaut beaucoup plus sûr.

Mais cela s'accompagne de compromis réels. RLS ne génère pas d'erreurs ; les requêtes renvoient silencieusement moins de lignes ou n'affectent rien. C'est plus sûr que de renvoyer toutes les données, mais cela rend le débogage plus difficile. Une UPDATE qui devrait modifier 100 lignes pourrait silencieusement en affecter 0 en raison d'une non-concordance de politique, et il est difficile de distinguer cela de « aucune donnée n'existe ».

Le pooling de connexions ajoute également de la complexité. RLS avec SET ne fonctionne pas correctement avec pgBouncer en mode de pooling de requêtes ou de transactions, ce qui peut entraîner le retour de lignes pour le mauvais tenant. Cela peut ne se manifester qu'en production.

Il existe également des limitations structurelles. Les super-utilisateurs contournent entièrement toutes les politiques, et les vues contournent RLS par défaut, votre application doit donc se connecter en tant que rôle non-super-utilisateur. Enfin, c'est spécifique à Postgres. Si vous devez prendre en charge MySQL, SQLite pour le développement, ou un autre magasin de données, votre couche de sécurité ne vous suivra pas.

Le constat pragmatique : RLS est excellent comme filet de sécurité pour l'isolation des tenants, mais la complexité opérationnelle et la difficulté de débogage signifient que ce n'est pas une solution prête à l'emploi pour chaque équipe.

La place de Zen

Ce sont toutes des approches valides, et si vous en utilisez une, c'est excellent. La protection IDOR de Zen est conçue pour un scénario différent : vos requêtes passent par un pilote de base de données, directement ou via un ORM, et vous voulez un filet de sécurité qui fonctionne quel que soit l'ORM, le constructeur de requêtes ou le modèle SQL brut que vous utilisez, sans modifier votre configuration de base de données ni adopter une bibliothèque de framework spécifique.

Zen a ses propres compromis, pour être honnête. Comme acts_as_tenant, il vous demande d'appeler setTenantId à chaque requête. Si vous oubliez, Zen génère une erreur, il échoue donc bruyamment plutôt que silencieusement, mais c'est le même type de configuration par requête. Et contrairement à RLS, Zen ne couvre que les requêtes qui s'exécutent au sein de votre application. Si quelqu'un se connecte directement à la base de données, par exemple via psql ou un service séparé sans Zen, ces requêtes ne sont pas vérifiées.

Il est également agnostique au langage par conception. Le moteur d'analyse SQL étant écrit en Rust, nous le compilons en WebAssembly pour Node.js et Go, ainsi qu'en une bibliothèque native que d'autres agents appellent via FFI. La protection IDOR sera spécifiquement disponible pour les agents Python, PHP, Go, Ruby, Java et .NET également.

Comment Zen protège contre les IDORs

Zen s'intègre à votre application et analyse les requêtes SQL à l'exécution, avec un contexte complet sur l'origine de la requête.

Un parseur SQL robuste, écrit en Rust

Au cœur de la protection IDOR de Zen se trouve un véritable parseur SQL basé sur le crate sqlparser en Rust, compilé en WebAssembly pour Node.js et Go. Il parse le SQL de la même manière qu'une base de données, en construisant un arbre syntaxique abstrait (AST) complet de votre requête, puis parcourt l'arbre pour en extraire :

  • Quelles tables la requête interroge (y compris les alias)
  • Quels filtres d'égalité sont présents dans la clause WHERE
  • Quelles colonnes et valeurs sont utilisées dans les instructions INSERT

Pourquoi pas les regex ? Les regex fonctionnent bien pour les requêtes simples comme SELECT * FROM orders WHERE tenant_id = ?. Mais les applications réelles comportent des CTE, des UNIONs, des sous-requêtes, des JOINs avec des alias, et toutes sortes de SQL valide avec lesquelles une approche basée sur les regex rencontre des difficultés. À mesure que les requêtes deviennent plus complexes, le parsing basé sur les regex devient de plus en plus fragile. Ce n'est pas nécessairement faux, mais c'est difficile à maintenir et facile d'être surpris par des comportements inattendus.

Un parseur robuste gère tout cela nativement. Il reconnaît également correctement les instructions qui ne nécessitent pas de vérification, telles que les instructions DDL (CREATE TABLE, ALTER TABLE), le contrôle des transactions (BEGIN, COMMIT, ROLLBACK), et les commandes de session (SET, SHOW).

Voici comment l'analyse fonctionne en interne. Étant donné cette requête :

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

Le parseur produit :

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

Zen vérifie ensuite si chaque table de la requête possède un filtre sur tenant_id, et si la valeur du filtre correspond au tenant actuel.

Il en va de même pour INSERT, UPDATE, et DELETE. Zen s'assure que la colonne `tenant` est toujours présente et contient toujours la bonne valeur. Ces cas sont traités comme des erreurs, et non simplement journalisés. L'IDOR est un bug de développeur, pas une attaque externe, il est donc préférable qu'il soit détecté bruyamment pendant le développement et les tests plutôt que de passer inaperçu en production.

Performance

Analyser le SQL à chaque requête peut sembler coûteux, mais en pratique, c'est rapide. L'idée clé est que la plupart des applications utilisent des requêtes préparées ou paramétrées. La chaîne SQL reste la même, seules les valeurs des paramètres changent. Ainsi, SELECT * FROM orders WHERE tenant_id = $1 AND status = $2 est analysée une seule fois, et chaque exécution ultérieure de cette même requête est un succès de cache.

La première fois que Zen rencontre une nouvelle chaîne de requête, le parseur Rust construit l'AST et en extrait les tables et les filtres. Chaque fois par la suite, il s'agit simplement d'une recherche dans le cache et d'une comparaison de l'ID du tenant avec la valeur de l'espace réservé résolu.

Si vous intégrez des valeurs directement dans des chaînes SQL, par exemple par concaténation de chaînes au lieu de requêtes paramétrées, chaque chaîne unique nécessite une nouvelle analyse. Mais vous ne devriez probablement pas faire cela de toute façon. Les requêtes paramétrées protègent contre les injections SQL et, par la même occasion, accélèrent la vérification des IDOR.

Le chemin vers la production : l'application de nos propres solutions

Nous avons déployé la protection IDOR de Zen sur plusieurs services internes d'Aikido. Cela a immédiatement mis en évidence des cas limites qui nécessitaient d'être gérés.

Le support des transactions a été un obstacle au début. Les applications réelles utilisent BEGIN, COMMIT, et ROLLBACK, et Zen devait les reconnaître comme des instructions sûres qui ne nécessitent pas de filtrage par tenant, plutôt que de générer des erreurs. Nous avons ajouté cette fonctionnalité rapidement après l'avoir vue échouer lors de notre premier déploiement interne.

Les expressions de table communes (CTE) ont été un autre défi. Une CTE comme WITH active AS (SELECT * FROM orders WHERE tenant_id = $1) crée une table virtuelle à laquelle les requêtes en aval font référence. Zen devait suivre les noms des CTE et les exclure de la liste des « tables réelles » tout en analysant le corps de la CTE pour un filtrage approprié.

Le withoutIdorProtection La porte de secours s'est également avérée essentielle. Toutes les requêtes ne nécessitent pas un filtrage par tenant, comme les tableaux de bord d'administration, les tâches de fond ou les analyses inter-tenants. Nous avons initialement essayé une approche ignoreNextQuery où l'on appelait une fonction avant la requête pour ignorer la vérification pour la prochaine instruction SQL :

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

Cela s'est avéré fragile en pratique. Avec les pools de connexions, la « prochaine requête » sur une connexion donnée pourrait ne pas être celle que vous aviez l'intention d'ignorer. L'approche basée sur un callback withoutIdorProtection est explicite quant à la portée. La protection IDOR est désactivée pour la durée du callback et rien d'autre.

Comment nous avons protégé notre API qui sert les données d'actifs cloud

L'un des services que nous avons protégés dès le début était l'API interne qui sert les données d'actifs cloud à travers la plateforme.

Cette API est utilisée par l'UI, les tâches de fond et plusieurs moteurs de sécurité chaque fois qu'ils ont besoin de lire des informations sur l'infrastructure d'un client. Elle est au cœur du système et gère des milliers de requêtes par seconde.

Étant donné que la plateforme est entièrement multi-tenant, une isolation stricte des tenants est essentielle. Chaque requête doit être limitée à la bonne organisation, et nous ne pouvons pas compter sur les développeurs pour se souvenir d'ajouter le bon filtre dans chaque chemin de code.

Avant que Zen ne prenne en charge nativement la protection IDOR, nous avions une implémentation personnalisée qui appliquait la limitation des tenants au niveau de la requête. Une fois que Zen a introduit une prise en charge native de ce comportement, nous avons migré de notre solution développée en interne vers la fonctionnalité intégrée, ce qui représente considérablement moins de code à maintenir pour nous.

Aujourd'hui, Zen vérifie automatiquement que les requêtes sont correctement limitées au tenant actuel, même sous forte charge. Après l'introduction de la protection IDOR de Zen, nous n'avons constaté aucun impact notable sur les performances.

Détection et prévention

L'AI Pentest d'Aikido trouve les vulnérabilités IDOR dans votre application en cours d'exécution en simulant de véritables attaques. La protection IDOR de Zen les empêche d'être introduites dès le départ en détectant les filtres de tenant manquants au moment du développement.

Ensemble, ils couvrent les deux aspects. L'AI Pentest valide que votre code existant est sécurisé, et Zen garantit que le nouveau code reste sécurisé. Utilisez l'AI Pentest pour auditer ce qui est déjà déployé. Utilisez Zen pour détecter les erreurs au fur et à mesure que vous les écrivez.

Démarrer

La protection IDOR est disponible dès aujourd'hui dans @aikidosec/firewall pour Node.js. Consultez le guide de configuration pour commencer. Le support pour d'autres langages arrive bientôt !

Partager :

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

Abonnez-vous pour les actualités sur les menaces.

4,7/5
Fatigué des faux positifs ?
Essayez Aikido, comme 100 000 autres.
Commencez maintenant
Obtenez une démonstration personnalisée

Approuvé par plus de 100 000 équipes

Réserver maintenant
Analysez votre application à la recherche d'IDORs et de chemins d'attaque réels

Approuvé par plus de 100 000 équipes

Démarrer l'analyse
Découvrez comment le pentest IA teste votre application

Approuvé par plus de 100 000 équipes

Démarrer les tests

Sécurisez votre environnement dès maintenant.

Sécurisez votre code, votre cloud et votre environnement d’exécution dans un système centralisé unique.
Détectez et corrigez les vulnérabilités rapidement et automatiquement.

Aucune carte de crédit requise | Résultats en 32 secondes.