La fuite silencieuse que votre runbook ne détecte pas
Une application multi-tenant bien construite, une politique de sécurité au niveau des lignes (Row Level Security), des variables de session pour isoler chaque client : c'est exactement ce que la plupart des guides recommandent. Et c'est précisément cette combinaison qui, passée par un pooler de connexions comme PgBouncer en mode transaction, crée une faille de confidentialité sérieuse.
Ce qui se passe réellement dans le pooler
En mode transaction, PgBouncer réattribue les connexions PostgreSQL entre les clients à chaque fin de transaction. L'objectif est louable : réduire le nombre de connexions ouvertes, économiser des ressources, améliorer la scalabilité. Le problème survient lorsque la stratégie d'isolation des tenants repose sur des variables de session comme app.tenant.
Voici le scénario classique. Une politique RLS lit current_setting('app.tenant', true) pour filtrer les lignes. Chaque requête commence par un SET app.tenant = 'identifiant_du_client'. Sur une connexion dédiée, tout fonctionne correctement. À travers le pooler, la première transaction positionne la variable, la connexion retourne dans le pool, une seconde transaction non reliée récupère cette même connexion, interroge current_setting('app.tenant', true) et obtient la valeur laissée par le client précédent. Sans avoir rien configuré, sans s'authentifier, elle lit les lignes de l'autre tenant.
Ce n'est pas un bug de PostgreSQL. C'est le comportement attendu du mode transaction : le cycle de vie de la connexion backend dépasse celui de la transaction, et les variables de session persistent sur la connexion, pas sur la transaction.
Le protocole MCP amplifie le problème
Depuis le 28 juillet 2026, le protocole MCP (Model Context Protocol) ne porte plus de session propre par défaut. Chaque appel d'outil d'un agent arrive comme une requête indépendante, sans continuité de session. Un agent multi-tenant qui appelle un outil via MCP émet exactement le type de connexion vulnérable décrit ci-dessus : une transaction courte, sans garantie sur la connexion backend qu'elle obtient.
Concrètement, un agent traitant une demande pour le tenant A peut, si la connexion recyclée portait déjà une variable app.tenant = 'B' et que rien ne la réinitialise correctement, lire les documents, les factures ou les contrats du tenant B. La fuite est silencieuse, aucune erreur n'est levée, les logs n'indiquent rien d'anormal.
L'article source de Mikhail Shytsko détaille ce comportement avec une reproduction précise, connexion par connexion : Your Agent Is Reading Someone Else's Tenant.
Les trois correctifs à appliquer
Passer en mode session sur PgBouncer
Le correctif le plus direct consiste à basculer PgBouncer en pool_mode = session. En mode session, une connexion backend reste associée à un client pendant toute la durée de sa connexion. Les variables de session ne peuvent plus fuir vers un autre client. Le coût : moins de mutualisation des connexions, un nombre de backends potentiellement plus élevé. Pour beaucoup d'applications, c'est un compromis acceptable face au risque.
Réinitialiser les variables en début de transaction
Si le mode transaction est conservé pour des raisons de performance, chaque transaction doit commencer par réinitialiser explicitement les variables sensibles avant de les définir. Un RESET app.tenant ou un SET app.tenant = '' suivi du SET légitime garantit qu'une valeur résiduelle ne persistera pas. Cette approche est fragile : elle dépend de la discipline de chaque point d'entrée du code, et un oubli suffit à rouvrir la faille.
Déplacer l'isolation au niveau de la requête
Plutôt que de s'appuyer sur une variable de session, certaines architectures passent l'identifiant du tenant directement dans les requêtes SQL, via un paramètre de fonction ou une colonne de filtrage explicite couplée à RLS. Cette approche n'est pas affectée par le recyclage des connexions, mais elle impose de revoir la politique RLS et potentiellement une partie du code applicatif.
Auditer son infrastructure avant d'activer des agents
Avant de brancher un agent IA sur une base multi-tenant, trois vérifications s'imposent :
- Identifier le mode de pooling utilisé (
pool_modedans la configuration PgBouncer). - Vérifier si les politiques RLS s'appuient sur des variables de session (
current_setting,SET LOCALversusSET). - Tester le comportement en simulant deux connexions successives sur le même backend, sans réinitialisation, pour observer ce qu'hérite la seconde.
La commande SHOW pool_mode; côté PgBouncer et SELECT current_setting('app.tenant', true); depuis une nouvelle connexion sans SET préalable suffisent à confirmer ou infirmer la présence de la fuite.
Ce que ça change pour vous
Si votre application ou votre site sert plusieurs clients sur la même base de données, il existe peut-être une configuration technique qui permet à un utilisateur de voir les données d'un autre. Ce n'est pas forcément visible dans les logs, et ça ne déclenche aucune alerte automatique. Avec l'arrivée des assistants IA connectés à vos outils métier, ce type de faille peut être déclenché sans action humaine directe. Un audit technique de la configuration de votre base de données est une précaution raisonnable avant de déployer des agents automatisés sur des données sensibles.
Et votre site, il vaut quoi ?
Vitesse, accessibilité, référencement technique. Le rapport est écrit pour un dirigeant, pas pour un développeur. Gratuit, sans inscription.