Accueil / Blog / Gouvernance et IA

Gouvernance et IA

Lecture seule sur l'arbre SQL, pas sur le premier mot

Sommaire
  1. Ce que le premier mot ne dit pas
  2. La garde que nous avons gardée
  3. Ce que la garde ne remplace pas

Écrit et testé avec akko-mcp-trino 0.6.0. La version actuelle est la 0.7.2. Voir ce qui a changé.

Un serveur MCP qui expose un moteur SQL à des agents promet presque toujours la même chose : il est en lecture seule. La promesse est simple à formuler et difficile à tenir. La plupart des implémentations que nous lisons la tiennent avec un filtre sur le premier mot-clé : si la requête commence par SELECT, SHOW ou EXPLAIN, elle passe ; sinon, elle est refusée. Nous avons commencé de la même façon dans akko-mcp-trino, puis nous avons cherché à casser notre propre garde. Voici les six défauts que nous avons rencontrés, dans notre code ou dans celui des autres, et la forme que la garde a prise ensuite.

Ce que le premier mot ne dit pas

Un filtre par préfixe répond à la question « par quoi commence le texte ? ». La bonne question est « que fera le moteur si je lui envoie ce texte ? ». Les deux questions n’ont la même réponse que dans les cas simples. Chaque défaut ci-dessous est un cas où elles divergent.

1. Les requêtes empilées. SELECT 1; DROP TABLE t commence par SELECT. Selon le pilote et le moteur, la seconde instruction est exécutée, refusée, ou silencieusement ignorée. Une garde ne doit pas dépendre de ce comportement. Elle exige exactement une instruction et refuse le reste.

2. Une écriture dans un CTE. WITH w AS (DELETE FROM t) SELECT 1 commence par WITH, que le filtre autorise, et sa racine est bien un SELECT. Nous avons trouvé ce cas le 12 septembre sur notre propre garde, avec la preuve adverse que nous rejouons sur un cluster réel : la garde ne regardait que le nœud racine de l’arbre, et l’écriture enveloppée atteignait Trino. Ce n’est pas du SQL Trino valide : dans la grammaire de Trino, le corps d’un CTE est une requête de lecture, et Trino l’a refusée comme une erreur de syntaxe. Mais sqlglot l’analyse, avec un SELECT à la racine. Une garde ne doit pas compter sur le parseur d’en face : elle parcourt tout l’arbre.

3. Une écriture dans une sous-requête. SELECT * FROM (DROP TABLE t) x est la même idée sous une autre forme. Ce n’est pas du SQL Trino valide non plus, et Trino le refuse. sqlglot ne sait pas l’analyser, et la garde le refuse donc par le principe du cas 6. La conclusion est la même : il faut parcourir tout l’arbre, et refuser dès qu’un nœud d’écriture apparaît, où qu’il soit.

4. EXPLAIN ANALYZE exécute. Sur Trino, EXPLAIN planifie une requête sans la lancer, mais EXPLAIN ANALYZE la lance pour en mesurer le plan. EXPLAIN ANALYZE INSERT INTO t VALUES (1) commence par EXPLAIN et écrit une ligne. Nous avons trouvé ce cas le 13 septembre en lisant la garde d’un autre serveur, et le nôtre avait le même trou. La règle est devenue : EXPLAIN est autorisé si et seulement si l’instruction expliquée est elle-même une lecture, et ANALYZE est toujours refusé.

5. Les commandes qui ne s’appellent pas des écritures. GRANT, SET SESSION, CALL ne modifient pas une table et ne figurent pas dans la liste habituelle des verbes interdits. GRANT change qui peut lire quoi. SET SESSION change le comportement des requêtes suivantes de la session. CALL lance une procédure dont personne ne connaît les effets à l’avance. Une garde en lecture seule les refuse tous les trois.

6. Ce que le filtre ne sait pas lire. /* note */ DELETE FROM t commence par /*. Un texte que l’analyseur ne comprend pas, ou qui ne correspond à aucune forme connue, doit être refusé, pas transmis au moteur en espérant qu’il tranche. C’est le principe du refus par défaut : l’incertitude ferme, elle n’ouvre pas.

La garde que nous avons gardée

La version actuelle tient en une fonction. Elle analyse le texte avec sqlglot dans le dialecte Trino, exige une seule instruction, puis parcourt l’arbre entier et refuse dès qu’un nœud appartient à la famille des écritures et des effets de bord (Insert, Update, Delete, Merge, Create, Drop, Alter, Truncate, Grant, Set, commande générique). SHOW et DESCRIBE, que sqlglot ne structure pas, sont acceptés par leur mot-clé de tête, et EXPLAIN est réanalysé sur l’instruction qu’il enveloppe. Toute exception de l’analyseur vaut refus. Elle refuse aussi les lectures hors des tables gouvernées : les table functions et le catalog system. Voici le cœur de is_read_only_sql, tel qu’il est dans la version 0.6.0.

statements = [st for st in sqlglot.parse(s, read="trino") if st is not None]
if len(statements) != 1:  # exactly one statement, never stacked queries
    return False
st = statements[0]
if isinstance(st, _READ_TYPES):
    # A read at the root is not enough: refuse any write nested anywhere.
    if any(isinstance(node, _WRITE_TYPES) for node in st.walk()):
        return False
    return not _reads_outside_governed_tables(st, session_catalog)

Chacun des six cas ci-dessus est un test dans le dépôt, et la preuve adverse en rejoue une partie sur un cluster réel à chaque livraison. Nous ne considérons pas la liste comme close. C’est la raison pour laquelle nous l’avons publiée.

Ce que la garde ne remplace pas

Une garde en lecture seule côté serveur est une seconde ligne. La première reste le moteur, qui applique ses propres permissions à la personne pour qui l’agent travaille. C’est ce que fait akko-mcp-trino en portant l’identité de l’utilisateur jusqu’à Trino ; nous l’avons décrit dans notre premier article. Si le moteur reçoit chaque requête sous la bonne identité, une écriture qui passerait la garde serait encore refusée à qui n’a pas le droit d’écrire. Si le moteur reçoit tout sous un compte de service, la meilleure garde du monde protège une porte qui n’a plus de serrure.

La garde a aussi ses angles morts. Les tables de métadonnées d’un connecteur, comme "orders$files" ou "orders$partitions" pour Iceberg, se lisent comme des tables ordinaires du même catalog, et la garde les laisse passer. C’est à la policy du moteur de les viser.

Le code est sur GitHub, sous licence Apache 2.0, et le paquet s’installe avec pip install akko-mcp-trino. Si vous cassez la garde, ouvrez une issue : c’est exactement ce que nous cherchons.

Partager cet articleRéagir sur LinkedIn ↗
AKKO

AKKO est un projet français de plateforme data et IA souveraine, installée sur votre infrastructure. Chaque exemple de nos articles est testé avant publication.