Blog SQLFix
Langage naturel vers SQL : vérifications de sécurité qui comptent
Traduire une question en SQL est vraiment utile pour les premières ébauches, mais l'exécution automatique de la sortie du modèle contre une base de données live est comment un erreur de frappe devient une table supprimée. La posture sécurisée est la seule ébauche : le modèle propose, un humain ou un pipeline protégé dispose. SQLForm suit ce modèle d'ébauche uniquement et n'exécute jamais ce qu'il génère sans une étape de révision.
1. Commencez avec un pipeline en mode brouillon
J'ai conçu SQLForm autour de l'idée qu'un modèle de langage est un assistant de rédaction, et non un moteur d'exécution. Lorsqu'un utilisateur demande « montrez-moi les ventes du mois dernier par région », le modèle renvoie une instruction SELECT. Cette instruction est placée dans une file d'attente de brouillons et n'est jamais envoyée au connecteur de production. Un opérateur humain ou une porte automatique doit explicitement approuver le brouillon avant qu'il ne quitte la file d'attente. Cette séparation crée une traçabilité claire : chaque requête qui touche les données a une étape d'approbation enregistrée.
La file d'attente de brouillons est persistante dans une table légère avec des colonnes pour l'ID utilisateur, l'horodatage, la invite brute, le SQL généré et le statut d'approbation. Si le système plante, la file d'attente peut être rejouée et aucune requête n'est perdue. J'ai choisi cette conception après avoir vu des équipes perdre des données parce qu'un script de démonstration exécutait une instruction DELETE que le modèle pensait être une instruction SELECT. La file d'attente empêche cette classe d'erreur par construction.
2. Détecter les instructions de mutation avant qu'elles n'atteignent la file d'attente
La première ligne de défense est un simple scan lexical. Je recherche les jetons DELETE, UPDATE, INSERT, DROP, TRUNCATE, ALTER, GRANT, REVOKE et CREATE. Si l'un de ces jetons apparaît, le brouillon est marqué comme mutant et placé dans un état d'attente qui nécessite une approbation explicite d'un réviseur senior. Même un SELECT qui contient une clause WITH qui appelle une fonction de mutation est détecté car le scan est effectué sur la chaîne SQL brute avant tout parsing.
Je vérifie également les séparateurs d'instructions. Un point-virgule suivi d'un autre jeton signifie que l'utilisateur pourrait essayer de chaîner des opérations. Par exemple, « SELECT * FROM users ; DELETE FROM logs » passerait un test SELECT-seulement naïf, mais est bloqué car le scanner voit le jeton DELETE après le premier point-virgule.
Pour éviter les faux positifs sur les noms d'objets légitimes (comme une colonne nommée « delete_flag »), le scanner ne traite les jetons que comme des mots-clés lorsqu'ils sont suivis d'un espace ou d'une parenthèse, correspondant à la grammaire SQL typique. Cela permet de garder la règle précise sans interdire les identificateurs utiles.
3. Exécuter des brouillons dans un bac à sable en lecture seule
Une fois qu'un brouillon est approuvé, j'exécute celui-ci contre une réplique en lecture seule du schéma de production. La réplique est actualisée chaque nuit à partir du principal et aucune autorisation d'écriture n'est assignée au rôle de connexion utilisé par SQLForm. Si le modèle produit une instruction de mutation, la base de données renvoie une erreur et aucune donnée n'est modifiée. Cela transforme une erreur potentiellement coûteuse en un échec visible que le réviseur peut voir immédiatement.
Je force également la propriété en lecture seule au niveau de la connexion en définissant le paramètre PostgreSQL default_transaction_read_only = on pour la session. Dans MySQL, je démarre la session avec SET SESSION TRANSACTION ISOLATION LEVEL READ ONLY;. Ces paramètres obligent le serveur à rejeter toute commande d'écriture, d'altération ou de suppression avec un code d'erreur clair, que je fais apparaître dans l'interface utilisateur.
Utiliser une réplique a un avantage supplémentaire : la requête peut s'exécuter sur des données légèrement obsolètes sans affecter les SLA, et je peux surveiller la durée de la requête et l'utilisation des ressources sans risquer de surcharger la production. Si la réplique accuse un retard supérieur à un seuil configurable, le système bloque le brouillon et notifie l'approbateur que les données peuvent être désynchronisées.
4. Paramétrer les requêtes approuvées
Après qu'un brouillon ait franchi l'étape de révision humaine, je déplace le SQL dans la couche d'application sous la forme d'une instruction préparée. Le texte généré par le modèle devient un modèle où toutes les valeurs fournies par l'utilisateur sont liées en tant que paramètres. Je n'ai jamais concaténé de chaînes brutes, même après approbation, car cela réintroduit le risque d'injection que le brouillon était censé éviter.
Je suivre les conseils de la feuille de triche de prévention des injections SQL d'OWASP, qui liste les instructions préparées comme principale défense. Dans la pratique, j'utilise l'API native du pilote de base de données : pg_prepare / pg_execute pour PostgreSQL, et PreparedStatement pour MySQL. Le pilote prend soin de l'échappement et de la sécurité des types, donc une valeur contenant une guillemet ou un point-virgule est traitée comme des données, et non comme du SQL.
Pour m'assurer que le modèle reste valide, j'analyse le SQL approuvé avec un analyseur SQL léger (comme sqlparse) et je vérifie que chaque espace réservé correspond à un index de paramètre. Si l'analyseur trouve un jeton errant qui ressemble à une valeur mais n'est pas lié, le brouillon est renvoyé pour révision.
5. La revue humaine comme point de contrôle
Le réviseur n'est pas censé être un expert SQL ; la liste de contrôle est délibérément courte. Tout d'abord, il vérifie que la liste SELECT correspond aux colonnes mentionnées dans l'invite d'origine. Si l'utilisateur a demandé le « chiffre d'affaires total par région » et que la requête renvoie COUNT(*), le réviseur le signale. Deuxièmement, il confirme qu'aucun jeton de mutation n'apparaît, en s'appuyant sur le scanner mais également en effectuant un examen visuel pour toute obfuscation astucieuse. Troisièmement, il jette un coup d'œil à la clause WHERE pour voir si elle extrait involontairement plus de lignes que prévu (par exemple, en manquant un filtre de date qui entraînerait un balayage de la table entière).
Je traite la requête générée comme une suggestion, exactement comme je réviserais du SQL écrit par un nouveau membre de l'équipe. Le réviseur peut approuver, demander une modification ou rejeter. S'il demande une modification, il peut éditer directement le brouillon dans l'interface utilisateur ; la version éditée devient un nouveau brouillon et passe par les mêmes vérifications à nouveau. Cette boucle garantit que la sortie du modèle n'est jamais l'autorité finale.
Le gain de rapidité vient de l'élimination de la nécessité d'écrire du SQL de base à partir de zéro. J'ai vu des équipes réduire le temps nécessaire pour produire une requête de premier jet de dix minutes à moins de deux minutes, tout en gardant un humain dans la boucle pour des raisons de sécurité.
Ce que cela n'est pas
Je ne prétends pas que SQLForm détecte toutes les erreurs possibles. L'analyseur lexical peut être contourné par un codage inhabituel ou en utilisant une syntaxe spécifique à la base de données qui ressemble à un commentaire. Un attaquant déterminé pourrait créer une invite qui fait sortir du modèle une instruction qui utilise une procédure stockée avec des effets secondaires, que l'analyseur ne signalerait pas car le nom de la procédure n'est pas dans notre liste de mots clés. La réplique en lecture seule protège contre les écritures accidentelles, mais si la réplique est mal configurée et hérite des droits d'écriture, le filet de sécurité disparaît.
La révision humaine est essentielle, mais les réviseurs peuvent manquer des erreurs logiques subtiles, surtout lorsque la requête est longue ou utilise des fonctions de fenêtres complexes. Le processus réduit les risques, mais il ne les élimine pas. Je traite SQLForm comme un outil de réduction des risques, et non comme un remède miracle.
Références
https://cheatsheetseries.owasp.org/cheatsheets/LLM_Prompt_Injection_Prevention_Cheat_Sheet.html
https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html
https://cwe.mitre.org/data/definitions/798.html
https://cwe.mitre.org/data/definitions/89
FAQ
Uniquement contre une réplique en lecture seule ou avec une porte de sécurité qui bloque les instructions de modification. L'exécution automatique de SQL généré contre la production est la façon dont une erreur de brouillon devient une perte de données. Gardez le modèle dans un rôle de proposition uniquement.
Un brouillon de modèle est une entrée non fiable jusqu'à ce qu'il soit examiné, de la même classe que l'injection SQL CWE-89. Un rôle en lecture seule signifie qu'une exécution accidentelle ne peut pas modifier les données de production.
Non. Une fois qu'une requête est intégrée au code de l'application, utilisez des instructions préparées. La concaténation de valeurs dans une chaîne réintroduit le risque d'injection. L'examen et la paramétrisation sont des contrôles distincts.
Les instructions DELETE, UPDATE, DROP, TRUNCATE et d'octroi de privilèges ne devraient jamais s'exécuter dans un chemin d'exécution automatique. Ils nécessitent une approbation explicite et distincte.
Essayez le produit : SQLForm
2026-09-16 · sources primaires uniquement · pas de métriques de trafic fabriquées
Articles liés
Requêtes paramétrées vs SQL généré
Explorez la matrice complète des sujets de micro-SaaS IA →
Le NL2SQL peut-il être sécurisé pour une exécution automatique ?
Uniquement contre une réplique en lecture seule ou avec une porte de sécurité qui bloque les instructions de modification. L'exécution automatique de SQL généré contre la production est la façon dont une erreur de brouillon devient une perte de données. Gardez le modèle dans un rôle de proposition uniquement.
Pourquoi un contexte en lecture seule est-il important ? Un brouillon de modèle est une entrée non fiable jusqu'à ce qu'il soit examiné, de la même classe que l'injection SQL CWE-89. Un rôle en lecture seule signifie qu'une exécution accidentelle ne peut pas modifier les données de production. L'examen du SQL signifie-t-il que je peux ignorer la paramétrisation ?
Un brouillon de modèle est une entrée non fiable jusqu'à ce qu'il soit examiné, de la même classe que l'injection SQL CWE-89. Un rôle en lecture seule signifie qu'une exécution accidentelle ne peut pas modifier les données de production.
Quelles instructions une couche de sécurité doit-elle bloquer par défaut ?
Les instructions DELETE, UPDATE, DROP, TRUNCATE et les instructions d'octroi de privilèges ne doivent jamais s'exécuter dans un chemin d'exécution automatique. Ils nécessitent une approbation explicite et séparée.
Support de décision uniquement. Ne remplace pas l'examen du DBA ou les tests de charge de production. Aucune garantie de performances.