Le WordPress d'aujourd'hui, décodé pour les développeurs

Sécurité

Row-level security pour un multisite de 500 sites communaux

Sur un réseau mutualisé de 500 sites communaux, garantir qu'une commune ne peut jamais lire les données d'une autre exige une architecture de cloisonnement explicite, pensée dès la conception.

Par WordPress Développement • 31 mai 2022 • 4 min de lecture • Aucun commentaire
Row-level security pour un multisite de 500 sites communaux

Cinq cents sites communaux, une seule installation WordPress en mode multisite, une infrastructure mutualisée pour limiter les coûts d’hébergement et de maintenance à l’échelle d’un syndicat intercommunal. Cette mutualisation pose une question d’architecture centrale, souvent sous-estimée au moment de la conception : comment garantir qu’un agent de la commune A ne peut, en aucune circonstance, consulter ou modifier une donnée appartenant à la commune B, alors même que les deux sites partagent la même base de données et le même code applicatif ?

Le multisite WordPress natif répond partiellement à cette question pour les contenus standards (articles, pages, utilisateurs), chaque site disposant de ses propres tables préfixées. Mais dès qu’une fonctionnalité personnalisée introduit une table partagée entre tous les sites du réseau, par exemple pour centraliser des demandes de démarches administratives ou des réservations de salles municipales, le cloisonnement natif ne suffit plus : c’est à l’architecture applicative de le garantir explicitement.

Pourquoi une table partagée casse le cloisonnement natif

Une table personnalisée créée en dehors du système de préfixage par site (par exemple pour centraliser des statistiques d’usage entre communes, ou pour un annuaire d’équipements partagés) contient par construction les données de toutes les communes du réseau. Sans un filtre explicite appliqué à chaque requête, n’importe quelle route ou requête mal conçue peut retourner des lignes appartenant à une autre commune que celle de l’utilisateur connecté, ce qui a été précisément le cas identifié lors de la conception de ce projet, avant sa mise en production, grâce à une revue d’architecture préalable.

L’architecture retenue : un identifiant de site systématique

Le principe adopté repose sur une règle simple mais appliquée sans exception : toute table partagée entre plusieurs sites du réseau porte une colonne blog_id, et toute requête qui lit ou écrit dans cette table filtre explicitement sur cette colonne, sans jamais s’appuyer sur le contexte implicite du site courant fourni par WordPress, qui peut être manipulé ou mal restauré dans certains contextes (tâche planifiée, appel REST, import en ligne de commande).

┌─ Réseau multisite (une base de données)
│
├─ wp_blogs                     (registre des 500 sites communaux)
│
├─ wp_demarches_communes         (table partagée personnalisée)
│   ├─ id
│   ├─ blog_id      ← filtre obligatoire sur chaque requête
│   ├─ administre_id
│   ├─ type_demarche
│   └─ statut
│
└─ Pour chaque site :
    ├─ wp_2_posts, wp_2_options, …   (commune n°2, cloisonnement natif)
    ├─ wp_3_posts, wp_3_options, …   (commune n°3, cloisonnement natif)
    └─ …
L'essentiel à retenir : Le multisite WordPress ne cloisonne pas nativement les données personnalisées ; Chaque requête doit filtrer explicitement par identifiant de site ; Un audit croisé périodique reste nécessaire même après la mise en place du cloisonnement

Centraliser le filtre plutôt que le répéter

Plutôt que de faire confiance à chaque développeur pour ajouter manuellement la condition WHERE blog_id = %d à chaque requête, une classe d’accès aux données centralise ce filtre, rendant son omission structurellement difficile plutôt que dépendante de la vigilance individuelle :

class DemarchesRepository {
    public function getPourSiteActuel(array $filtres = []) {
        global $wpdb;
        $blog_id = get_current_blog_id();

        $sql = "SELECT * FROM wp_demarches_communes WHERE blog_id = %d";
        return $wpdb->get_results($wpdb->prepare($sql, $blog_id));
    }
}

Cette classe constitue le seul point d’accès autorisé à la table partagée dans le code applicatif : aucune requête directe vers wp_demarches_communes n’est tolérée en dehors de ce repository, ce qui facilite considérablement les revues de code ultérieures, un seul endroit à vérifier plutôt qu’une recherche exhaustive dans l’ensemble du code.

L’audit croisé, un filet de sécurité indispensable

Même avec cette architecture, un audit trimestriel automatisé interroge délibérément la table partagée en se plaçant dans le contexte de plusieurs sites différents, pour vérifier qu’aucune donnée d’une autre commune n’apparaît jamais dans les résultats retournés. Cet audit a permis, six mois après la mise en production, de détecter une nouvelle fonctionnalité développée par un prestataire externe qui avait contourné le repository central en interrogeant directement la table, réintroduisant temporairement le risque initial.

En résumé

Sur un multisite mutualisé à grande échelle, le cloisonnement des données ne peut pas reposer uniquement sur les mécanismes natifs de WordPress dès qu’une table personnalisée partagée entre plusieurs sites entre en jeu. Centraliser l’accès à ces tables dans un repository unique, filtré systématiquement par identifiant de site, et vérifier ce cloisonnement par un audit croisé périodique, transforme une promesse de conception en garantie réellement vérifiée dans le temps.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Partager :

À propos de l'auteur

WordPress Développement

Développeur WordPress, passionné par Elementor, le FSE et l’automatisation par IA.

Voir tous ses articles

Dans la même veine

À lire aussi