Retour aux textes

Le prompt n'est pas une frontière de sécurité

Quand un LLM écrit la requête, autoriser celui qui pose la question ne suffit pas. Ce qui compte, c'est ce que la réponse peut toucher.

Dans les systèmes qui génèrent du SQL, des filtres ou du code avec un modèle de langage, il y a deux questions d'autorisation, et il est facile de ne répondre qu'à la première :

  • Qui peut demander la requête ?
  • Que peut toucher la requête générée ?

Autoriser la première et ignorer la seconde, c'est fermer la porte à clé et laisser la fenêtre ouverte. L'utilisateur avait le droit de poser une question sur les données de son unité ; le SQL que le modèle a écrit, lui, n'en sait rien.

Ne chargez que ce qui a déjà été autorisé

La protection la plus forte contre un modèle qui accède à ce qu'il ne devrait pas, c'est que la donnée ne soit pas là. Au lieu d'ouvrir une engine avec tous les contextes et de compter sur le modèle pour choisir le bon, l'exécution ne reçoit que ce qui est déjà passé par l'autorisation :

ctx = authorize_data_context(user, context_id)  # prédicat côté serveur
engine = load_engine_only(ctx.storage_prefix)

Si le processus de cette exécution ne voit pas les autres datasets, aucun prompt, aussi créatif soit-il, ne fera arriver le modèle jusqu'à eux.

Le system prompt est une suggestion

« Utilise uniquement ces tables » dans le system prompt n'est pas un contrôle. Le modèle est un composant probabiliste et finira, tôt ou tard, par faire autre chose, que ce soit par erreur ou parce que quelqu'un l'a demandé avec les bons mots. La sortie doit passer par une validation déterministe avant l'exécution :

question
  -> contexte autorisé
  -> le modèle génère un candidat
  -> parser / AST / validation
  -> refus si invalide
  -> audit
  -> exécution sur une engine scopée
  -> réponse

Le guard minimum

Pour du SQL généré, le minimum que je considère acceptable :

  • un seul statement ;
  • lecture seule, avec DDL, DML et commandes d'administration bloqués ;
  • aucune fonction de fichier ni de réseau ;
  • toute relation référencée dans l'allowlist du contexte ;
  • limite de lignes, timeout et budget mémoire ;
  • connexion read-only, même si le guard échoue.

Le dernier point est celui qui permet de dormir tranquille : le guard est une couche, la connexion read-only en est une autre et l'engine scopée une troisième. Aucune n'a besoin d'être parfaite toute seule.

Les regex sont un échafaudage

On peut commencer avec des regex, à condition qu'elles soient couvertes par des tests adverses. Mais une regex est un plancher temporaire, pas une destination. Les commentaires, les chaînes, les identifiants entre guillemets et la syntaxe propre à chaque dialecte rendent n'importe quelle regex fragile :

SELECT * FROM "public"."Patients" /* ; DROP */
SELECT * FROM report WHERE note = 'FROM secrets'

Un parser compatible avec le dialecte qui va exécuter la query transforme la question « est-ce que ça a l'air dangereux ? » en « quelles relations cet arbre référence-t-il ? », et cette seconde question a une réponse exacte.

L'injection vient de la donnée

La question de l'utilisateur n'est pas la seule entrée non fiable. Nom de colonne, valeur de cellule, métadonnée de tableur, texte de PDF ingéré : tout ça arrive dans le contexte du modèle et peut contenir quelque chose qui ressemble à une instruction. Délimiter ces éléments comme des données aide, mais la défense décisive reste la validation de la sortie. Si la réponse ne peut être qu'un SELECT sur des relations autorisées, une instruction cachée dans un tableur n'a nulle part où aller.

Moins d'outils, moins de surface

Sur le chemin des requêtes sensibles, l'agent ne reçoit que le nécessaire. Si la tâche est de la lecture analytique, pas d'écriture sur le filesystem, pas de réseau arbitraire, pas de shell, pas de Python libre, pas de mutation de la base ni d'identifiants d'un autre service. Le moindre privilège vaut pour les agents comme pour les personnes, et à plus forte raison : l'agent lit tout ce qui passe devant lui.

Ça rejoint Masquez avant d'appeler le LLM : une couche s'occupe de ce que le modèle voit, l'autre de ce qu'il peut faire.