Passer au contenu
DedicatedPHP Contact

Isolation des données multi-tenant en PHP sans fuites

Concevez un SaaS PHP qui empêche les accès croisés entre organisations grâce à un contexte explicite et des contrôles sur les données, files d’attente, cache et tests.

Schéma éditorial d’un SaaS PHP avec des organisations isolées dans la base de données, le cache, les fichiers et les files d’attente

L’isolation des données multi-tenant en PHP ne se résout pas en ajoutant une condition WHERE organization_id = ? sur l’écran principal. Une fuite peut provenir d’une API, d’une exportation, d’un cache, d’une pièce jointe, d’un consommateur de file d’attente ou d’un processus planifié. Elle peut également survenir lorsqu’un administrateur légitime change d’organisation et que le système conserve un contexte antérieur.

L’objectif architectural doit être clair : aucune opération qui lit, modifie, traite ou fournit des informations client ne doit pouvoir agir sans un périmètre organisationnel vérifiable. Ce périmètre doit être propagé explicitement et validé à chaque frontière pertinente de l’application.

Ce qu’une application SaaS doit isoler

Ce qu’une application SaaS doit isoler — guía visual de DedicatedPHP

Le modèle transactionnel n’est qu’une partie de la surface de risque. Inventoriez les ressources ayant un propriétaire organisationnel et définissez, pour chacune, comment son appartenance est identifiée, stockée, récupérée, supprimée et auditée.

  • Données transactionnelles : utilisateurs, projets, commandes, factures, configurations et relations entre entités.
  • Fichiers et pièces jointes : objets dans un stockage externe, miniatures, documents générés et leurs métadonnées.
  • Cache : résultats de requêtes, autorisations calculées, sessions, réponses d’API et données de configuration.
  • Index de recherche : documents indexés, suggestions et filtres préalablement agrégés.
  • Traitement asynchrone : travaux de file d’attente, tentatives, lots d’importation et notifications.
  • Exploitation et observabilité : journaux, traces, métriques, exportations de support et outils internes.

Toutes les ressources ne requièrent pas la même stratégie. Un catalogue public peut être partagé, tandis qu’une facture, son PDF et les journaux de téléchargement doivent conserver un lien univoque avec l’organisation. La décision doit être documentée afin d’éviter qu’une nouvelle entité soit créée sans règles de propriété.

Choisir le modèle d’isolation des données

Il existe trois modèles courants. Aucun n’est universellement supérieur : le choix dépend des exigences réglementaires, du volume, de l’exploitation, du modèle commercial et de la capacité de l’équipe à maintenir la plateforme.

Base de données partagée avec clé d’organisation

Toutes les organisations partagent les tables et chaque enregistrement soumis à l’isolation contient une clé telle que organization_id. C’est l’approche la plus directe pour faire évoluer le produit et exécuter des requêtes agrégées globales. En contrepartie, elle exige une discipline extrême : chaque requête, relation, index, cache et tâche doit respecter le périmètre.

Au minimum, utilisez des clés étrangères lorsque cela est pertinent, des index composites commençant par organization_id et des contraintes d’unicité également composites. Par exemple, un code de commande unique au sein d’une organisation ne doit pas être déclaré globalement unique si telle n’est pas la règle métier.

Schéma séparé par organisation

Chaque client opère dans un schéma logique distinct au sein du même serveur de base de données. Cela réduit le risque d’omettre un filtre dans des tables séparées, mais complique les migrations, les connexions, les outils d’analyse et les requêtes globales. Cette approche n’est appropriée que si le moteur, le framework et l’exploitation quotidienne prennent ce modèle en charge de manière cohérente.

Base de données par organisation

Séparer les bases de données offre une frontière plus forte et peut faciliter les restaurations ou les déplacements de clients individuels. Cela augmente également l’inventaire des connexions, migrations, sauvegardes, de la supervision et des déploiements de modifications de structure. Il convient d’évaluer en particulier comment seront exécutés les rapports globaux, les modifications de masse et la récupération après erreur.

La séparation physique réduit certaines classes de défaillances, mais ne remplace ni l’autorisation, ni le contrôle des fichiers, ni la gestion des secrets, ni la validation du contexte dans les services partagés.

Architecture de référence : contexte explicite aux frontières

Le contexte organisationnel ne doit pas être déduit de paramètres arbitraires envoyés par le navigateur. Il doit être résolu à partir d’une source authentifiée et autorisée : un sous-domaine validé, un jeton doté d’une audience appropriée, une appartenance utilisateur ou un moyen d’authentification d’intégration associé à une seule organisation.

Dans une application PHP, une couche d’entrée peut construire un objet de contexte immuable avec l’identifiant de l’organisation, l’acteur, ses autorisations et un identifiant de requête. Les contrôleurs, commandes de console et consommateurs de file d’attente reçoivent ce contexte ou le reconstruisent à partir de données vérifiées. Évitez les variables globales mutables susceptibles de persister indûment dans des processus de longue durée.

final class OrganizationContext {
    public function __construct(
        public readonly string $organizationId,
        public readonly string $actorId
    ) {}
}

Les repositories doivent exiger le contexte pour interroger ou modifier des entités isolées. Une interface qui rend son omission difficile est préférable à une convention implicite qui dépend de la mémoire de chaque développeur. Lorsque cela est possible, appliquez aussi des politiques d’accès dans la couche domaine : appartenir à une organisation n’autorise pas automatiquement toute action en son sein.

Éviter les filtres oubliés dans les requêtes et relations

Une requête isolée doit filtrer par organisation avant de rechercher selon des identifiants métier. Récupérer d’abord un enregistrement par id, puis vérifier ensuite son propriétaire peut entraîner des expositions si le résultat est sérialisé, journalisé ou utilisé avant son rejet.

  • Centralisez les requêtes dans des repositories ou services de lecture dont les méthodes reçoivent le contexte.
  • Interdisez les accès directs aux modèles isolés depuis les contrôleurs, templates et consommateurs d’événements.
  • Examinez les relations : une relation chargée de manière différée peut contourner le filtre appliqué à l’entité principale.
  • Utilisez des contraintes de base de données pour empêcher les relations entre lignes d’organisations distinctes lorsque le modèle le permet.
  • Définissez des conventions pour les migrations, les jeux de données de test et les requêtes analytiques.

Dans les moteurs proposant des politiques de sécurité au niveau des lignes, celles-ci peuvent apporter une défense supplémentaire. Toutefois, leur adoption doit inclure des tests de connexion, la gestion des rôles et l’examen des processus administratifs. Il ne convient pas de supposer qu’une politique de base de données protège automatiquement les fichiers, le cache ou les index externes.

Risques hors du flux web principal

Les identifiants opaques réduisent l’énumération, mais n’autorisent pas l’accès. Un UUID ou un identifiant aléatoire doit toujours être résolu dans l’organisation active. De même, une URL de téléchargement signée requiert un objet appartenant au bon périmètre, une expiration appropriée et des règles de révocation lorsque les autorisations changent.

Les clés de cache doivent inclure l’identifiant de l’organisation et, lorsque le contenu dépend des autorisations, une dimension supplémentaire de rôle ou de version d’autorisation. Une clé telle que dashboard:summary est non sécurisée dans un environnement multi-tenant ; une clé avec un périmètre explicite permet aussi des invalidations plus précises.

Les exportations sont particulièrement sensibles car elles sont généralement exécutées en dehors de la requête initiale. Enregistrez qui l’a demandée, pour quelle organisation, quels filtres ont été approuvés et où le résultat sera livré. N’envoyez pas de pièces jointes ou de liens à des destinataires calculés à partir de données non validées.

Propager le contexte dans les API, webhooks et files d’attente

Une API doit dériver l’organisation du moyen d’authentification ou vérifier que la ressource demandée appartient à l’organisation associée à ce moyen d’authentification. Autoriser un en-tête X-Organization-Id peut être valide pour des opérateurs disposant d’une délégation explicite, mais requiert une autorisation spécifique, un audit et une interface qui rende visible le changement de périmètre.

Les webhooks entrants ne doivent pas faire confiance à un identifiant d’organisation inclus dans le corps sans vérifier la signature, l’émetteur et l’association préalable de l’intégration. Pour les webhooks sortants, générez des événements à partir de données déjà délimitées et évitez de réutiliser des charges utiles depuis une file d’attente partagée sans valider le destinataire.

Chaque travail asynchrone doit transporter un identifiant d’organisation avec l’identifiant de la ressource et reconstruire le contexte avant toute requête. Le consommateur doit vérifier les deux valeurs, même si le travail a été créé en interne. Les tentatives, travaux différés et tâches planifiées nécessitent la même règle : aucun contexte de requête implicite n’est disponible de manière sûre.

Tests et signaux de diagnostic vérifiables

Le test le plus important n’est pas qu’une organisation voie ses propres données, mais qu’elle ne puisse ni lire ni modifier celles d’une autre. Créez deux organisations avec des données délibérément similaires et exécutez des tests d’intégration sur chaque point d’entrée : interface web, API, commandes, exportations, téléchargements et consommateurs de file d’attente.

  • Demandez une ressource de l’organisation B à l’aide d’une session ou d’identifiants d’accès de l’organisation A et attendez une réponse non révélatrice.
  • Tentez de mettre à jour, supprimer, télécharger et exporter des ressources croisées, et pas seulement de les consulter.
  • Vérifiez que les clés de cache de A et B génèrent des résultats indépendants.
  • Exécutez un travail de file d’attente avec une ressource d’une autre organisation et vérifiez qu’il échoue de manière contrôlée.
  • Testez les restaurations, importations et tâches nocturnes avec les données de plus d’une organisation.
  • Journalisez les actions sensibles avec l’acteur, l’organisation, la ressource et le résultat, sans introduire de données personnelles inutiles dans les journaux.

Les tests basés sur les propriétés peuvent compléter les cas manuels : pour toute ressource créée sous une organisation, aucun acteur sans appartenance valide ne devrait pouvoir l’observer ou la modifier par une route exposée. Cette propriété doit s’appliquer aux évolutions futures des endpoints et des repositories.

Plan d’adoption pour une application existante

Si les données sont déjà mélangées, ne commencez pas par réécrire toute l’application. Inventoriez d’abord les entités, flux, intégrations et accès administratifs. Définissez ensuite la propriété de chaque enregistrement et résolvez les cas ambigus avec des règles métier révisables.

  1. Ajoutez l’entité organisation et la clé d’appartenance aux tables ciblées.
  2. Renseignez cette clé au moyen d’une migration contrôlée et conservez la preuve des cas sans attribution fiable.
  3. Introduisez des repositories délimités et des tests d’accès croisé dans les routes les plus sensibles.
  4. Incluez le périmètre dans le cache, les fichiers, les recherches et les nouveaux travaux.
  5. Migrez progressivement les anciens flux et bloquez les nouvelles requêtes sans contexte lors de la revue de code.
  6. Activez des contrôles plus stricts lorsque les métriques et les tests démontrent une couverture suffisante.

Décisions à documenter avant de grandir

Décisions à documenter avant de grandir — guía visual de DedicatedPHP

Avant d’intégrer l’organisation suivante, documentez le modèle choisi, la source de vérité du contexte, les exceptions d’accès administratif, la stratégie d’identifiants, les limites du cache, la propriété des fichiers, la récupération des données, la rétention des journaux et la procédure en cas de suspicion d’accès croisé.

Déterminez également qui peut agir au nom d’une autre organisation, comment cette délégation est approuvée et comment elle est révoquée. L’isolation des données multi-tenant en PHP se maintient grâce à des décisions explicites, des contraintes techniques reproductibles et des tests qui transforment une promesse d’architecture en comportement vérifiable.

Vous souhaitez appliquer ces idées à votre projet ?Parlons de votre plateforme PHP.
Afficher les services associés