Tester l'isolation multi-tenant d'un SaaS
Douze familles de tests tirées du vrai code : IDOR, webhooks, rate limiting, sessions. Ce qu'elles prouvent, et ce qu'elles ne prouvent pas.
Pourquoi une fuite entre tenants arrive
Un filtre organizationId oublié ne se voit ni en revue de code rapide ni en recette manuelle : les deux organisations de test ont chacune leurs propres données, personne ne clique exprès sur l'ID d'une autre. La fuite dort jusqu'au jour où un client curieux (ou malveillant) change un identifiant dans l'URL.
Les causes concrètes, dans l'ordre où elles arrivent vraiment :
findUniqueau lieu defindFirst: sonwheren'accepte que des champs uniques, doncorganizationIdne peut pas y être ajouté. Un filtre automatique côté lecture (type$extendsPrisma) ne le couvre pas.updateoudeletesansorganizationIddans lewhere: le filtre automatique ne s'applique qu'aux lectures. Une écriture qui ne le répète pas explicitement modifie n'importe quelle ligne dont on connaît l'ID.- Sous-entité sans
organizationIddirect : une ligne de facture, un message dans une conversation. Sans filtre imbriqué vers le parent, l'ID de la ligne suffit à y accéder. - Chemin secondaire non couvert : un webhook, un job planifié, un export, une recherche plein texte. Le router principal est propre, mais personne n'a vérifié le chemin qui contourne la procédure habituelle.
Une checklist n'empêche pas ces oublis. Un test automatisé qui échoue à la moindre régression, si.
Douze familles de tests, tirées du vrai code
Ce sont les fichiers réellement présents dans le produit (src/__tests__/security/), pas une liste générique. Chacun couvre un chemin d'attaque précis.
cross-tenant-isolation: deux organisations mockées, on vérifie que chaque lecture (factures, devis, conversations, événements d'agenda) filtre parorganizationIdavant même de toucher une base réelle.idor-comptabilite,idor-conversations,idor-messages,idor-notifications: un test par domaine métier sensible, parce qu'un filtre correct sur les factures ne dit rien sur les messages.idor-iopole-events: les événements entrants d'une intégration tierce (webhooks de plateforme de facturation électronique) sont aussi une surface IDOR, pas seulement les routes internes.webhook-security: la vérification de signature (Stripe, plateformes de facturation) compare le HMAC avec une comparaison à temps constant, jamais un===qui fuite l'information par le temps de réponse.open-redirect: un paramètre?next=non validé peut rediriger vers un domaine externe après connexion. Classique, oublié.rate-limiting: protection contre l'abus (force brute, spam de requêtes coûteuses), pas seulement contre l'IDOR.session-security: la configuration des cookies de session (NextAuth v5 change leur préfixe entre les versions), le genre de détail qui casse silencieusement l'authentification en production sans jamais planter en développement.env-security: aucun secret serveur ne doit fuiter dans le bundle client. Une variable d'environnement mal préfixée suffit à l'exposer.comprehensive-audit: un test générique qui parcourt tous les modèles scopés par organisation et vérifie que chaquefindManypasse par le filtre automatique ou déclare explicitementorganizationId. Il attrape un nouveau modèle qu'on aurait oublié d'ajouter à la liste des modèles scopés.
Le dernier point compte double : un test écrit à la main pour chaque modèle ne protège que les modèles déjà connus. Un test qui parcourt le schéma protège aussi les modèles qui n'existent pas encore.
NOT_FOUND, jamais FORBIDDEN
Sur une ressource qui n'appartient pas au tenant courant, la réponse ne doit jamais être « accès interdit ». Ça confirme que la ressource existe, avec un identifiant valide, dans une autre organisation.
// Mauvais : confirme l'existence de la ressource
if (invoice.organizationId !== ctx.session.user.organizationId) {
throw new TRPCError({ code: "FORBIDDEN" });
}
// Bon : indiscernable d'un ID qui n'existe pas
const invoice = await ctx.orgDb.invoice.findFirst({ where: { id } });
if (!invoice) {
throw new TRPCError({ code: "NOT_FOUND" });
}Un test d'IDOR qui vérifie uniquement expect(result).rejects.toThrow() passe aussi bien avec un FORBIDDEN qu'avec un NOT_FOUND. Il faut vérifier le code précis, sinon le test protège contre le bug qu'il ne fallait pas confondre avec la fuite d'information.
Automatiser plutôt que se souvenir
Ces tests n'ont de valeur que s'ils tournent à chaque changement, pas seulement avant un audit annuel. Trois conditions pour que ça tienne dans la durée :
- Dans la CI, sur chaque pull request, pas en local à la discrétion de chacun.
- Rapides, avec un contexte tRPC mocké plutôt qu'une vraie base de données : une suite de sécurité qui prend dix minutes finit par être désactivée sous pression de deadline.
- Un test générique par catégorie (comme
comprehensive-auditci-dessus) en plus des tests spécifiques, pour couvrir ce qu'on ajoute demain sans y penser aujourd'hui.
Ce que ces tests ne prouvent pas
Une suite verte n'est pas une preuve d'absence de faille, seulement l'absence des failles qu'on a pensé à tester. Ce qu'elle ne couvre pas :
- Les vulnérabilités de dépendances tierces (à suivre séparément, avec un outil dédié).
- Les erreurs de configuration d'infrastructure (permissions de bucket, règles réseau).
- L'ingénierie sociale et le phishing, hors du périmètre du code.
- Tout chemin d'attaque auquel personne n'a encore pensé : c'est pour ça qu'un audit externe ponctuel reste utile en complément, pas à la place, des tests automatisés.
Pour aller plus loin
Articles connexes
Tester un SaaS multi-tenant : 7 patterns Vitest indispensables
De l'isolation tenant à la vérification des permissions RBAC, les patterns de test qui rendent un SaaS B2B vraiment fiable. Avec Vitest + tRPC + Prisma.
LireMulti-tenant avec Prisma et tRPC : isoler les données par organisation
Isoler les données de chaque organisation dans Next.js avec Prisma $extends et tRPC : ce que le filtre automatique couvre, et ce qui reste explicite.
LireAuthentification multi-tenant avec NextAuth v5
Implémentez un système d'authentification robuste avec rôles, permissions granulaires et isolation multi-tenant pour votre SaaS.
LirePrêt à lancer ton SaaS ?
HeartCo Starter inclut tout ce dont tu as besoin : auth, paiements, IA, mobile, sécurité auditée. À partir de 199 €.