TL;DR
Les IDOR sont la principale cause de fuite de données pour les entreprises SaaS multi-locataires, et elles sont généralement découvertes après le déploiement. Aikido rend l'isolation des locataires obligatoire. Zen analyse toutes vos requêtes SQL lors de l'exécution à l'aide d'un analyseur SQL approprié (écrit en Rust), vérifie que chaque requête filtre le bon locataire et renvoie une erreur si ce n'est pas le cas. Les développeurs ne peuvent plus accidentellement fournir un 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 IDOR sont-ils plus dangereux aujourd'hui ?
Les vulnérabilités IDOR, également appelées « références directes non sécurisées à des objets », constituent l'une des failles les plus courantes et les plus dangereuses dans les applications multi-locataires. Elles surviennent lorsqu'une requête oublie de filtrer par locataire, permettant ainsi à un compte d'accéder aux données d'un autre compte.
Pendant longtemps, les IDOR étaient difficiles à détecter. Elles n'apparaissaient pas dans les analyses de code et nécessitaient beaucoup de travail manuel. De ce fait, de nombreux bugs IDOR n'étaient découverts qu'au cours de tests d'intrusion coûteux et laborieux, ou lorsque des chercheurs en sécurité les trouvaient grâce à des programmes de prime aux bugs.
Mais cela a changé. Les outils de test de sécurité agencés peuvent désormais se comporter comme de vrais utilisateurs, en cliquant sur les flux de travail, en changeant de rôle et en essayant automatiquement d'accéder aux 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 doivent pas se contenter de détecter les IDOR, mais aussi les prévenir.
Pourquoi la détection ne suffit pas
En matière de détection, Aikido AI Pentest Aikido est déjà capable de détecter les vulnérabilités IDOR, ce que SAST traditionnels basés sur des modèles ne peuvent pas faire de manière fiable, car l'IDOR nécessite de comprendre le contexte d'autorisation, et pas seulement les modèles de code. AI Pentest s'authentifie en tant qu'utilisateur réel, exécute des workflows complets et réutilise les identifiants d'objets entre les rôles afin de détecter les failles IDOR réellement exploitables. C'est pour cette raison que de nombreuses organisations qui utilisent notre fonctionnalité AI Pentest s'intéressent principalement à la détection des IDOR.
Mais détecter les vulnérabilités IDOR n'est qu'une partie du problème. Dans un monde idéal, il faudrait empêcher leur apparition dès le départ. C'est justement le sujet de cet article : la protection IDOR dans Zen, notre pare-feu intégré à l’application open source. Il analyse chaque requête SQL lors de l'exécution et renvoie une erreur si une requête ne comporte pas de filtre de locataire ou utilise un identifiant de locataire incorrect, ce qui permet de détecter 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 des fournisseurs, une question revient régulièrement : comment la multi-location est-elle mise en œuvre et comment l'accès aux données entre locataires est-il empêché ?
Les équipes de sécurité et la direction veulent des garanties techniques claires que les limites entre locataires sont appliquées de manière systématique, et 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 permet de passer d'une conversation du type « 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 comporte des comptes, des organisations, des espaces de travail ou des équipes, vous disposez probablement d'une colonne telle que identifiant_locataire qui conserve les données de chaque compte séparées. Mais lorsque la requête oublie de filtrer sur cette colonne ou filtre sur une valeur erronée, cela signifie qu'un compte peut accéder aux données d'un autre. Il s'agit d'une vulnérabilité IDOR.
Voici un exemple simple. Vous disposez d'un point de terminaison 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);
});Vous voyez le problème ? Il n'y a pas de identifiant_locataire filtre. Si Alice envoie GET /commandes/42 et que la commande 42 appartient à Bob, Alice reçoit la commande de Bob. C'est un IDOR.
La solution est simple : ajoutez un OÙ tenant_id = $2 clause. Cependant, ce bug est facile à introduire et difficile à détecter, en particulier dans une base de code volumineuse comportant des centaines de requêtes réparties dans des dizaines de fichiers. Il suffit d'un seul filtre manqué pour que le bug apparaisse.
IDOR est une catégorie très large. Elle inclut également des éléments tels que l'accès aux fichiers d'autres utilisateurs via la manipulation d'URL ou des points de terminaison 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 en savoir plus sur IDOR de manière plus générale, consultez notre article expliquant les vulnérabilités IDOR.
Ce qui existe aujourd'hui
Outre l'analyse de sécurité, qui vise davantage à détecter les bogues 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 présente des avantages et des limites.
Bibliothèques au niveau du cadre
Plusieurs frameworks disposent de bibliothèques qui limitent automatiquement les requêtes au locataire actuel :
- Ruby on Rails: acts_as_tenant ajoute automatiquement une portée locataire aux modèles ActiveRecord. Déclarez acts_as_tenant(:account) et toutes les requêtes sur ce modèle seront filtrées par le locataire actuel.
- Django: django-multitenant fait la même chose pour l'ORM de Django. Définissez le locataire actuel dans le middleware, et Product.objects.all() devient automatiquement limité au locataire.
- Laravel: Tenancy for Laravel offre une multi-location à base de données unique et multi-bases de données, avec changement de contexte automatique.
- .NET / EF Core: Filtres de requête globaux vous permettre d'appliquer
OÙ tenant_id = Xà chaque requête automatiquement au niveau du modèle.
Ces bibliothèques fonctionnent bien avec leur propre ORM. Leur limite est qu'elles ne protègent que les requêtes qui passent par l'abstraction de l'ORM. Les requêtes SQL brutes, les requêtes provenant d'autres bibliothèques ou les requêtes construites avec un autre générateur de requêtes dans le même projet ne seront pas prises en compte. Elles sont également facultatives, vous devez vous rappeler d'ajouter l'annotation à chaque modèle, et les nouveaux modèles peuvent passer inaperçus. Pour être honnête, agit_en_tant_que_locataire a un exiger_locataire config qui génère une erreur lorsqu'aucun locataire n'est défini, ce qui réduit considérablement le risque d'« oubli de définir le locataire ».
Il existe également des pièges subtils. Dans Rails, par exemple, agit_en_tant_que_locataire fonctionne en ajoutant un portée_par_défautSi un développeur appelle Projet.sans portée pour supprimer une autre portée par défaut, telle qu'une archivé filtre, il supprime toutes les portées par défaut, y compris le filtre de locataire, sans erreur ni avertissement. Rails a unscope (sans le d) pour supprimer chirurgicalement un seul scope, mais cela nécessite de savoir au préalable que le scope locataire existe. Dans une base de code impliquant de nombreux développeurs, quelqu'un finira par recourir à sans champ d'application, et la limite du locataire disparaît silencieusement.
Application au niveau de la base de données
Sécurité au niveau des lignes (RLS) de PostgreSQL va encore plus loin en imposant l'isolation des locataires au niveau de la base de données. Au lieu de compter sur votre application pour ajouter OÙ tenant_id = ? à chaque requête, vous demandez à 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 TOUS
UTILISANT (tenant_id = current_setting(« app.current_tenant_id ») ::uuid) ;
-- 3. Par requête, définissez le contexte du locataire avant d'exécuter les requêtes
SET app.current_tenant_id = 'aaaa-aaaa-aaaa';
-- Désormais, même une simple instruction SELECT renvoie uniquement les lignes de ce locataire.
SELECT * FROM projects;
RLS est la garantie la plus solide des approches énumérées ici. Qu'il s'agisse de requêtes SQL brutes ou ORM, cela n'a pas d'importance. La base de données l'impose. Et contrairement à agit_en_tant_que_locataireSi vous oubliez de définir la variable de session, aucune donnée n'est renvoyée au lieu de toutes les données. Il s'agit d'un paramètre par défaut beaucoup plus sûr.
Mais cela implique de réels compromis. RLS ne génère pas d'erreurs ; les requêtes renvoient silencieusement moins de lignes ou n'ont aucun effet. C'est plus sûr que de renvoyer toutes les données, mais cela rend le débogage plus difficile. Un MISE À JOUR qui devrait modifier 100 lignes pourrait en réalité n'en affecter aucune en raison d'une incompatibilité de politique, et il est difficile de distinguer cela de « aucune donnée n'existe ».
Le regroupement des connexions ajoute également à la complexité. RLS avec SET ne fonctionne pas correctement avec pgBouncer en mode de mise en pool des instructions ou des transactions, ce qui peut entraîner le renvoi de lignes pour le mauvais locataire. Ce problème peut uniquement apparaître en production.
Il existe également des limitations structurelles. Les superutilisateurs contournent entièrement toutes les politiques, et les vues contournent le RLS par défaut. Votre application doit donc se connecter en tant que rôle non superutilisateur. Enfin, cette solution est réservée à 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 suit pas.
Conclusion pragmatique : RLS est un excellent filet de sécurité pour l'isolation des locataires, mais sa complexité opérationnelle et sa difficulté de débogage font qu'il ne s'agit pas d'une solution prête à l'emploi pour toutes les équipes.
La place du zen
Ce sont toutes des approches valables, et si vous en utilisez une, c'est parfait. 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 souhaitez disposer d'un filet de sécurité qui fonctionne quel que soit l'ORM, le générateur de requêtes ou le modèle SQL brut que vous utilisez, sans modifier la configuration de votre base de données ni adopter une bibliothèque de framework spécifique.
Le zen a ses propres compromis, il faut être honnête. Comme agit_en_tant_que_locataire, cela vous oblige à appeler setTenantId à chaque requête. Si vous oubliez, Zen génère une erreur, ce qui entraîne un échec flagrant plutôt que silencieux, mais il s'agit de la même classe de configuration par requête. Et contrairement à RLS, Zen ne couvre que les requêtes exécutées au sein de votre application. Si quelqu'un se connecte directement à la base de données, par exemple via psql ou un service distinct sans Zen, ces requêtes ne sont pas vérifiées.
Il est également indépendant du 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 bibliothèque native que d'autres agents appellent via FFI. La protection IDOR sera également disponible pour les agents Python, PHP, Go, Ruby, Java et .NET.
Comment Zen protège contre les IDOR
Zen s'intègre à votre application et analyse les requêtes SQL lors de leur exécution, en tenant compte du contexte complet de l'auteur de la requête.
Un analyseur SQL correct, écrit en Rust
Au cœur de la protection IDOR de Zen se trouve un véritable analyseur SQL basé sur le crate sqlparser en Rust, compilé en WebAssembly pour Node.js et Go. Il analyse le SQL de la même manière qu'une base de données le ferait en construisant un arbre syntaxique abstrait (AST) complet de votre requête, puis parcourt l'arbre pour extraire :
- Quelles tables la requête touche (y compris les alias)
- Quels filtres d'égalité se trouvent dans la clause WHERE ?
- Quelles colonnes et valeurs figurent dans les instructions INSERT ?
Pourquoi pas regex ? Regex fonctionne bien pour les requêtes simples telles que SELECT * FROM commandes WHERE identifiant_locataire = ?. Mais les applications réelles comportent des CTE, des UNION, des sous-requêtes, des JOIN avec des alias et toutes sortes de SQL valides qui posent problème à une approche basée sur les expressions régulières. À mesure que les requêtes deviennent plus complexes, l'analyse basée sur les expressions régulières devient de plus en plus fragile. Elle n'est pas nécessairement mauvaise, mais elle est difficile à maintenir et peut facilement réserver des surprises.
Un analyseur syntaxique adéquat gère tout cela dès son installation. Il reconnaît également correctement les instructions qui ne nécessitent pas de vérification, telles que les instructions DDL (CRÉER UNE TABLE, MODIFIER UNE TABLE), contrôle des transactions (BEGIN, COMMIT, ROLLBACK) et les commandes de session (SET, SHOW).
Voici à quoi ressemble l'analyse en coulisses. Étant donné cette requête :
SELECT * FROM commandes
GAUCHE JOIN articles_de_commande ON commandes.id = articles_commande.id_commande
WHERE commandes.tenant_id = $1
ET statut des commandes = « 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 dispose d'un filtre sur identifiant_locataire, et si la valeur du filtre correspond au locataire actuel.
Il en va de même pour INSÉRER, MISE À JOUR, et SUPPRIMERZen s'assure que la colonne tenant est toujours présente et contient toujours la bonne valeur. Ces erreurs sont signalées comme telles, et ne sont pas simplement enregistrées. L'IDOR est un bug de développement, et non une attaque externe. Il est donc préférable qu'il soit signalé clairement pendant le développement et les tests plutôt que de passer inaperçu jusqu'à la mise en production.
Performance
L'analyse syntaxique SQL à chaque requête semble coûteuse, mais dans la pratique, elle est rapide. Le point essentiel à retenir est que la plupart des applications utilisent des instructions préparées ou des requêtes paramétrées. La chaîne SQL reste la même, seules les valeurs des paramètres changent. Ainsi, SELECT * FROM commandes WHERE identifiant_locataire = $1 AND statut = $2 est analysée une seule fois, et chaque exécution ultérieure de cette même requête est un accès au cache.
La première fois que Zen voit une nouvelle chaîne de requête, l'analyseur Rust construit l'AST et 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 locataire avec la valeur du paramètre fictif résolu.
Si vous intégrez des valeurs directement dans des chaînes SQL, par exemple en concaténant des chaînes au lieu d'utiliser des requêtes paramétrées, chaque chaîne unique nécessite une nouvelle analyse. Mais vous ne devriez probablement pas procéder ainsi de toute façon. Les requêtes paramétrées protègent contre les injections SQL et permettent également d'accélérer la vérification IDOR.
Le chemin vers la production : manger notre propre nourriture pour chiens
Nous avons déployé la protection IDOR de Zen sur plusieurs services internes Aikido. Cela a immédiatement mis en évidence des cas limites qui devaient être traités.
Le support des transactions a été un obstacle au début. Les applications réelles utilisent COMMENCER, ENGAGER, et ROLLBACK, et Zen devait reconnaître ces messages comme des messages sûrs ne nécessitant pas de filtrage par le locataire, plutôt que de les considérer comme des erreurs. Nous avons rapidement ajouté cette fonctionnalité après avoir constaté son échec lors de notre premier déploiement interne.
Les expressions de table communes (CTE) constituaient un autre défi. Une CTE telle que WITH active AS (SELECT * FROM commandes 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 continuant à analyser le corps des CTE pour un filtrage correct.
Le sans protection escape s'est également avérée essentielle. Toutes les requêtes ne nécessitent pas un filtrage des locataires, comme les tableaux de bord d'administration, les tâches en arrière-plan ou les analyses inter-locataires. Nous avons d'abord essayé un ignorer la requête suivante approche, où vous appelleriez une fonction avant la requête pour ignorer la vérification de la prochaine instruction SQL :
Zen.ignoreNextQuery();
const result = await db.query(« SELECT count(*) FROM orders »);
Cela s'est avéré fragile dans la pratique. Avec les pools de connexions, la « requête suivante » sur une connexion donnée peut ne pas être celle que vous aviez l'intention d'ignorer. Le système basé sur les rappels sans protection est explicite quant à sa portée. La protection IDOR est désactivée pendant toute la durée du rappel et rien d'autre.
Comment nous avons protégé notre API qui fournit des données cloud
L'un des services que nous avons protégés dès le début était l'API interne qui fournit les données cloud sur l'ensemble de la plateforme.
Cette API est utilisée par l'interface utilisateur, les tâches en arrière-plan et plusieurs moteurs de sécurité chaque fois qu'ils ont besoin de lire des informations sur l'infrastructure d'un client. Elle se trouve au cœur du système et traite des milliers de requêtes par seconde.
La plateforme étant entièrement multi-locataires, il est essentiel d'assurer une isolation stricte entre les locataires. Chaque requête doit être limitée à l'organisation appropriée, et nous ne pouvons pas compter sur les développeurs pour qu'ils n'oublient pas d'ajouter le filtre adéquat 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 portée des locataires au niveau des requêtes. Une fois que Zen a introduit une prise en charge de premier ordre pour ce comportement, nous sommes passés de la solution maison à la fonctionnalité intégrée, ce qui nous a permis de réduire considérablement la quantité de code à maintenir.
Aujourd'hui, Zen vérifie automatiquement que les requêtes sont correctement limitées au locataire actuel, même en cas de charge importante. Après avoir introduit la protection IDOR de Zen, nous n'avons constaté aucun impact notable sur les performances.
Détection et prévention
L'AI Pentest Aikido détecte les vulnérabilités IDOR dans votre application en cours d'exécution en simulant des attaques réelles. La protection IDOR de Zen empêche leur introduction en détectant les filtres de locataires manquants dès la phase de développement.
Ensemble, ils couvrent les deux aspects. AI Pentest vérifie que votre code existant est sûr, et Zen garantit que le nouveau code reste sûr. Utilisez 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 désormais disponible dans @aikidosec/firewall pour Node.js. Consultez le guide d'installation pour commencer. La prise en charge d'autres langages sera bientôt disponible !

