Fuite Multi-Tenant : Quand la "Row-Level Security" échoue dans le SaaS

Dans le monde du Software-as-a-Service (SaaS), il n’y a pas de catastrophe plus terminale que la “fuite de données”. C’est le “Code Rouge” de l’industrie — une défaillance catastrophique de l’isolation multi-tenant où le Client A se connecte et voit les dossiers financiers sensibles, les données personnelles ou la stratégie privée du Client B.
Depuis des années, les développeurs pointent la Row-Level Security (RLS) comme la solution définitive. La logique est simple : “Il suffit d’étiqueter chaque ligne avec un tenant_id et laisser la base de données gérer le reste.” Mais à mesure que les architectures SaaS deviennent plus complexes, se reposer uniquement sur la RLS devient un pari dangereux.
Dans cette analyse approfondie, nous dépassons les simples attaques de Broken Object Level Authorization (BOLA) pour explorer les défaillances architecturales — telles que la contamination du Connection Pool, le Poisoning du cache partagé, et les fuites de contexte asynchrone — qui peuvent faire échouer la RLS silencieusement, entraînant d’énormes expositions de données.
1. L’illusion de la Row-Level Security
La Row-Level Security (RLS) est une fonctionnalité de base de données (notamment dans PostgreSQL, SQL Server, et Oracle) qui permet de définir des politiques pour restreindre les lignes qu’un utilisateur peut voir ou modifier en fonction de son identité.
Pourquoi cela semble une solution miracle
Dans une configuration RLS typique, votre application établit une connexion et exécute une commande comme :
SET app.current_tenant_id = 'client_abc';
À partir de ce moment, chaque SELECT * FROM factures devient automatiquement SELECT * FROM factures WHERE tenant_id = 'client_abc'. La logique de sécurité passe de l’application (où les développeurs pourraient oublier une clause WHERE) au moteur de la base.
La faille fatale : c’est seulement aussi bon que le contexte
Le problème principal de la RLS est qu’elle est sans état en principe, mais avec un état en pratique. La base de données ne sait pas qui est “Client A” tant que l’application ne lui dit pas. Si le pont entre l’identité de l’application et le contexte de session de la base de données se brise, tout le modèle de sécurité s’effondre.
Dernières recherches : CVE-2024-10976 et au-delà
Des vulnérabilités récentes ont montré que la RLS n’est pas infaillible. CVE-2024-10976 a mis en évidence un scénario dans PostgreSQL où les politiques de sécurité des lignes sous les sous-requêtes pouvaient ignorer les changements d’ID utilisateur. De plus, l’avis CVE-2025-8713 a révélé que les statistiques de l’optimiseur pouvaient divulguer des données échantillonnées de lignes que la RLS était censée cacher. Ces “fuites d’informations” permettent à des attaquants astucieux d’inférer le contenu des données d’autres tenants via des analyses de canaux auxiliaires des plans de requête et des messages d’erreur.
2. Le fantôme dans la machine : contamination du Connection Pool
Dans le SaaS moderne, on n’ouvre pas une nouvelle connexion à la base pour chaque requête ; ce serait trop lent. À la place, on utilise le Connection Pooling (par exemple PgBouncer, HikariCP, ou Prisma). C’est là que la première grande défaillance architecturale se produit.
Comment la contamination se produit
Imaginez une application SaaS à fort trafic.
- Requête 1 (Tenant A) arrive. l’application récupère la connexion #42 du pool.
- L’application définit le contexte de session :
SET app.tenant_id = 'Tenant_A'. - La requête s’exécute, les données sont renvoyées, et la requête se termine.
- L’erreur critique : en raison d’une exception non gérée ou d’une erreur de codage, l’application ne “nettoie” pas la connexion avant de la rendre au pool.
- Requête 2 (Tenant B) arrive. Elle récupère la connexion #42.
- L’application suppose qu’il s’agit d’une nouvelle connexion ou ne surcharge pas immédiatement le tenant_id.
- La fuite : la requête 2 exécute
SELECT * FROM secretset — parce que la connexion #42 conserve encore l’état de Tenant A — la base de données sert les secrets de Tenant A à Tenant B.
La défaillance du “nettoyage”
De nombreux développeurs comptent sur le “middleware” pour définir et réinitialiser les tenant_id. Cependant, si un service backend plante ou si une transaction de base de données est interrompue en cours de route, la logique de “reset” peut ne jamais s’exécuter. Sans une commande RESET ALL ou DISCARD ALL imposée par le proxy de pooling lui-même, la connexion reste “empoisonnée” avec l’identité du précédent utilisateur.
3. Poisoning du cache partagé : quand Redis devient la fuite
Pour atteindre une latence inférieure au milliseconde, les applications SaaS s’appuient fortement sur des caches partagés comme Redis ou Memcached. Cela introduit une seconde couche, souvent invisible, de risque multi-tenant.
Collisions dans l’espace clé
L’échec le plus courant du cache est simple : ne pas préfixer les clés avec le tenant_id.
- Mauvais :
GET user_profile_123 - Bon :
GET tenant_A:user_profile_123
Mais même avec le préfixage, des “conditions de course” architecturales peuvent survenir.
La course dans le backend
Considérez un scénario où l’application utilise un pattern “Cache-Aside”. Lorsqu’une requête arrive, l’application vérifie Redis. Si c’est un miss, elle interroge la base et écrit dans Redis.
- Le tenant A demande son tableau de bord.
- L’application calcule les données du tableau, mais, à cause d’un bug dans la logique asynchrone, elle écrit le résultat dans une clé générique comme
latest_dashboard_stats. - Le tenant B demande son tableau de bord quelques millisecondes plus tard. Il atteint le cache et reçoit les données tout juste écrites par le tenant A.
Poisoning du cache partagé (perspective 2025)
En 2024 et 2025, une nouvelle frontière de fuite du cache a émergé : le service multi-tenant LLM. La recherche sur le partage de KV-Cache (comme l’attaque “PROMPTPEEK”) a montré que lorsque plusieurs utilisateurs partagent le même cache GPU sous-jacent pour l’efficacité, un utilisateur peut reconstituer les prompts d’un autre en analysant les hits de cache et les canaux auxiliaires de timing. Bien que spécifique à l’IA, cela illustre une vérité plus large : toute ressource partagée utilisée pour l’optimisation est une voie potentielle de fuite.
4. Le tueur silencieux : fuites de contexte asynchrone
Les backends SaaS modernes sont presque entièrement asynchrones (Node.js, Go, Python FastAPI). Ces langages utilisent des objets “contexte” pour transmettre les tenant IDs à travers une chaîne d’appels de fonctions sans “prop drilling”.
Le piège du thread unique
Dans Node.js, AsyncLocalStorage est la méthode standard pour suivre l’état du tenant. Cependant, si un développeur utilise une variable globale ou un singleton mal scoped pour stocker un tenant_id, il crée un risque majeur de fuite de données.
- Le scénario : Node.js gère des milliers de requêtes simultanées sur un seul thread.
- L’échec : si un développeur écrit accidentellement dans une variable globale partagée — même pour une microseconde — lors d’un bloc
await, toutes les autres requêtes concurrentes sur ce thread pourraient adopter cette valeur.
Une condition de course dans un service backend peut mener à un échange d’identité, où le “contexte” de la Requête A est accidentellement écrasé par celui de la Requête B parce qu’ils ont tous deux accédé à une ressource partagée mal isolée au niveau du thread ou de la tâche.
5. Au-delà de la RLS : stratégies pour un multi-tenancy renforcé
Si la RLS ne suffit pas, que faire ? Pour éviter la fuite de données, les architectes SaaS doivent adopter une approche de Defense-in-Depth.
1. Le “Reset” obligatoire pour le Connection Pool
Ne faites pas confiance à votre code applicatif pour nettoyer les connexions. Configurez votre proxy de pooling (comme PgBouncer) pour utiliser le “Session Pooling” avec une requête server_reset_query obligatoire. Chaque fois qu’une connexion est rendue au pool, le proxy doit exécuter DISCARD ALL pour effacer les tables temporaires, variables de session, et déclarations préparées.
2. Isolation cryptographique (le “Standard d’or”)
La défense ultime est de s’assurer que même si un tenant voit les données d’un autre, il ne peut pas les lire.
Chiffrement côté application (ALE) : chiffrer les colonnes sensibles avec une clé propre à chaque tenant.
Si le Tenant B récupère accidentellement la ligne du Tenant A via une connexion contaminée, il ne verra qu’un blob chiffré. Il ne possède pas la clé de déchiffrement de Tenant A (qui doit être stockée dans un KMS comme AWS KMS ou HashiCorp Vault).
3. Vérifications au niveau logique
Ne vous fiez jamais uniquement à la base pour la vérification. Même avec la RLS activée, votre code applicatif doit effectuer une vérification manuelle :
if record.tenant_id != current_user.tenant_id:
raise SecurityLeakError("Accès inter-tenant détecté!")
Cela peut sembler redondant, mais dans un environnement multi-tenant, la redondance est la seule voie vers la sécurité.
4. Cache tenant-aware avec ACLs
Si vous utilisez Redis 6.0 ou plus récent, cessez d’utiliser un seul mot de passe “admin”. Utilisez les ACLs Redis pour créer des utilisateurs spécifiques à chaque tenant, limités à des motifs de clés précis :
ACL SETUSER tenant_A on epassword ~tenant_A:* +get +set
En appliquant l’isolation au niveau du cache, vous vous assurez qu’une erreur dans votre logique applicative ne peut pas conduire à un “GET” inter-tenant.
6. Conclusion : Le paradoxe de la sécurité SaaS
Plus nous rendons nos architectures SaaS efficaces — via le connection pooling, le cache partagé, et l’exécution asynchrone — plus nous augmentons le risque de fuite multi-tenant.
La Row-Level Security est un outil puissant, mais ce n’est pas une solution complète. C’est un “filet de sécurité”, pas un “mur d’enceinte”. La véritable isolation des données nécessite une approche holistique qui considère la gestion d’état comme la principale frontière de sécurité.
Alors qu’on se dirige vers 2026, la complexité du SaaS ne fera qu’augmenter. Les organisations qui se contentent d’une seule couche de défense (comme la RLS) finiront par faire face à la redoutable “fuite de données”. Celles qui survivront seront celles qui ont construit leur architecture en partant du principe que le contexte de la base de données peut échouer, que le cache peut être empoisonné, et que la connexion peut être contaminée — et qui s’y sont préparées en conséquence.
Related InstaTunnel pages
Continue from this article into the most relevant product guides and workflows.
Related Topics
Keep building with InstaTunnel
Read the docs for implementation details or compare plans before you ship.