Sécurité

Sécurité du serveur MCP pour Craft CMS : la limite, c'est un utilisateur Craft.

Un client IA qui peut modifier un site en production a besoin de limites claires. Cette page liste chaque limite dans l'ordre où un appel la rencontre. Elle dit aussi ce que chaque limite ne couvre pas.

Ce que traverse chaque appel

Dans cet ordre. Un appel qui échoue à une étape n'atteint jamais l'outil.

  1. Limite de débit — appels par minute, comptés par jeton. Un frein contre un modèle qui tourne en boucle, pas une défense contre un appelant déterminé qui détient un jeton valide.
  2. Portée du jeton — read, content ou full. La portée ne fait que restreindre ce que l'utilisateur associé peut déjà faire.
  3. Édition — le plugin est livré en une seule édition, cette étape passe donc toujours aujourd'hui. Elle existe pour qu'un futur découpage en éditions ne puisse pas distribuer un outil en douce.
  4. Mode lecture seule — tant qu'il est activé, rien de ce qui modifie un état ne s'exécute, quoi que disent les paramètres ci-dessous.
  5. Activation des outils — chaque outil qui modifie un état reste désactivé jusqu'à ce qu'un administrateur l'active, un par un.
  6. Contrôle administrateur — les outils d'administration exigent en plus que l'utilisateur associé soit administrateur.
  7. Schéma des arguments — les arguments sont validés selon le schéma de l'outil avant que celui-ci ne les voie.
  8. Le contrôle de permissions de Craft, élément par élément — l'outil demande ensuite à Craft, pour cet élément et ce site, si l'utilisateur associé peut le voir, l'enregistrer ou le supprimer.

Deux types de refus, volontairement. Un outil qui n'existe pas et un outil que la portée du jeton ne couvre pas reçoivent la même réponse — Unknown tool — car la différence révélerait à un attaquant ce qui existe d'autre. Un outil désactivé, bloqué par le mode lecture seule ou réservé aux administrateurs n'est pas listé non plus, mais un appel direct reçoit la raison en clair, pour que l'exploitant ne cherche pas l'erreur de son côté.

Pourquoi l'utilisateur est la vraie limite

Chaque jeton appartient à exactement un utilisateur Craft. Deux questions sont posées, et c'est la seconde qui compte : d'abord la question générale, à savoir si ce type d'action est ouvert à cet utilisateur, puis la question précise — cet utilisateur peut-il faire cela, sur cet élément, sur ce site. Un rédacteur qui ne peut travailler que sur un site et ne voir qu'une section atteint exactement cela via le client IA, et rien d'autre.

Cela a une conséquence qu'il vaut mieux dire clairement : un jeton qui appartient à un administrateur peut faire ce que peut faire un administrateur. Attribuez un jeton à l'utilisateur le moins privilégié qui peut encore faire le travail, et utilisez la portée pour restreindre davantage.

Une section ou un groupe de catégories inconnu ou interdit donne une liste vide, pas une erreur — une erreur confirmerait que la chose existe. Un handle de site inexistant est en revanche signalé comme une erreur, car un handle erroné est une erreur dans l'appel, pas une limite.

Jetons

  • Le texte en clair est affiché une seule fois, à la création du jeton. Seul un hachage SHA-256 est stocké, il ne peut donc plus être affiché.
  • Les jetons vivent dans leur propre table, jamais dans la configuration du projet — qui est versionnée dans Git.
  • La page des jetons du panneau de contrôle est réservée aux administrateurs. Craft enregistre une permission « Access AlphaBridge MCP » pour chaque plugin doté d'une entrée dans la barre latérale ; l'accorder n'ouvre pas cette page aux non-administrateurs.
  • Un jeton peut avoir une date d'expiration et peut être révoqué à tout moment ; l'appel suivant avec ce jeton échoue.
  • L'en-tête Authorization est masqué dans les entrées de journal de Craft qui contiennent le contexte de la requête, pour qu'un jeton ne finisse pas dans storage/logs quand une requête est journalisée.

Le journal d'audit, et ce qu'il omet volontairement

Chaque appel d'outil est enregistré : quand, quel outil, quel utilisateur Craft, et le résultat — autorisé, refusé ou échoué. Les appels refusés sont enregistrés aussi ; ce sont les plus intéressants.

Le nom des arguments est enregistré, leurs valeurs ne le sont pas. Vous pouvez voir qu'une entrée a été créée avec une section et un titre ; vous ne pouvez pas lire le titre dans le journal. C'est voulu : un journal qui stockerait les valeurs serait une seconde copie, moins protégée, de vos contenus.

La lecture du journal nécessite la portée administrateur. Le journal est ramené à une taille fixe, il ne peut donc pas grossir sans limite.

Une limite, en toute honnêteté : l'écriture de l'entrée de journal se fait au mieux. Si cette écriture échoue elle-même — une erreur de base de données, par exemple —, l'appel n'est pas interrompu ; l'échec est consigné dans le journal de Craft. Considérez donc le journal d'audit comme un très bon registre, pas comme une garantie que rien ne peut manquer.

Lire les journaux de Craft est plus restreint qu'il n'y paraît

L'outil de journaux renvoie les entrées de journal de Craft sans le contexte de requête que Craft y attache. Ce contexte contient $_POST, $_COOKIE et $_SESSION — y compris, lors d'une connexion échouée, un mot de passe en clair. Les requêtes SQL et les traces de pile sont coupées au premier marqueur.

C'est pourquoi l'outil de journaux est un outil d'administration, désactivé par défaut.

Les outils de base de données, et leurs limites

Deux outils atteignent la base de données, et un seul exécute du SQL que vous écrivez. craft_db_schema liste les noms des tables et marque les tables protégées comme illisibles. craft_db_query exécute un seul SELECT sur une seule table et rejette, avant que quoi que ce soit n'atteigne la base de données :

  • les jointures, les sous-requêtes, les expressions de table communes et UNION
  • toute fonction sauf count, min, max, avg et sum
  • les tables protégées — utilisateurs, sessions, jetons, configuration du projet et autres tables qui contiennent des identifiants ou des secrets
  • les expressions qui nomment le compte du serveur ou une séquence

Une barrière faite de règles ne connaît que ce qu'on lui a dit. C'est pourquoi les deux sont des outils d'administration, pourquoi l'outil de requête est désactivé par défaut, et pourquoi aucun des deux n'écrit jamais.

Origines et DNS rebinding

Si une requête porte un en-tête Origin, celui-ci doit figurer sur la liste d'autorisation, sinon le point d'accès répond 403. La liste d'autorisation comprend ce que vous avez configuré plus l'URL du site, mais seulement là où celle-ci peut être déterminée statiquement. Une URL de site construite à partir de l'alias @web de Craft ne compte pas : Craft déduit @web de l'en-tête Host de la requête entrante, un attaquant serait donc comparé à une valeur qu'il a lui-même choisie.

Dans une telle installation, la liste configurée est la seule source, et si elle est vide, toute requête qui porte un Origin est refusée. Les clients de serveur à serveur n'envoient pas d'Origin et ne sont pas concernés.

Ce que ce plugin ne fait pas

Chacun de ces points est un choix, et chacun supprime une catégorie de risque au lieu de la gérer.

  • Pas d'exécution de code arbitraire. Aucun outil n'exécute de PHP, de commandes shell ou de template que vous lui transmettez.
  • Pas de déploiement ni de transfert de fichiers. Le plugin ne déplace pas de fichiers et ne touche jamais un serveur par SSH ou FTP.
  • Pas de gestion du cycle de vie des plugins. Rien n'installe, n'active, ne désactive ni ne supprime de plugins.
  • Pas d'écriture dans la configuration du projet ni dans les utilisateurs. Craft applique les règles correspondantes dans ses contrôleurs ; ce plugin parle à l'API des éléments, qui ne voit pas ces règles — il n'y écrit donc pas du tout.
  • Pas d'étiquettes (tags). Craft ne définit aucune permission pour les tags : toute limite devrait être inventée plutôt qu'appliquée.
  • Pas de valeurs de réglages de champs. Les réglages de champs ne sont renvoyés que par leur nom, jamais avec leurs valeurs, car un plugin peut y mettre n'importe quoi. Ce n'est pas une promesse générale sur les secrets : l'outil de base de données atteint toute table physique dont le nom ne figure pas sur sa liste protégée, et une table tierce au nom anodin n'y figure pas. Cet outil est réservé aux administrateurs et désactivé par défaut précisément pour cette raison.

Comment nous le garantissons dans la durée

Chaque modification déclenche une suite de tests unitaires, une analyse statique PHPStan niveau 6 sans baseline, et un test de bout en bout sur une installation Craft fraîchement construite sous PostgreSQL 16 et MySQL 8.4. Ce test crée un rédacteur non administrateur qui ne peut voir qu'une section et travailler que sur un site, et prouve que le filtre de permissions coupe vraiment — et pas seulement que les outils renvoient quelque chose.

Vous avez trouvé quelque chose ? Signalez-le en privé à mail@cultureclub.dev, pas dans une issue publique.