29 déc. 2025·5 min de lecture

Injection SQL dans les applications CRUD générées par l'IA : modèles et correctifs

Comprenez comment repérer l'injection SQL dans des applications CRUD générées par l'IA, avec des exemples concrets de requêtes vulnérables et des remplacements plus sûrs utilisant des paramètres et les fonctionnalités des ORM.

Injection SQL dans les applications CRUD générées par l'IA : modèles et correctifs

À quoi ressemble une injection SQL dans les apps CRUD

L'injection SQL se produit lorsqu'une application laisse l'entrée utilisateur modifier le sens d'une requête vers la base de données. Au lieu d'être traitée comme de simples données, l'entrée devient du SQL. Cela peut exposer des données privées, modifier des enregistrements ou effacer des tables.

Les endpoints CRUD sont des cibles fréquentes parce qu'ils acceptent constamment des entrées : champs de recherche, filtres, formulaires d'édition, identifiants dans les URLs et écrans d'administration. Les attaquants n'ont rien de compliqué à fournir. Ils ont juste besoin d'un endroit où une entrée est intégrée dans une requête.

Dans beaucoup de prototypes générés par l'IA, les zones risquées paraissent « normales » au premier abord. Les générateurs de code ont tendance à utiliser des concaténations rapides de chaînes, surtout autour de la recherche, des filtres et du tri. Ils ont aussi tendance à rassembler l'analyse de la requête, la construction SQL et le formatage de la réponse dans une seule fonction longue, ce qui rend facile de rater une ligne dangereuse.

Signes qu'une route CRUD pourrait être injectable :

  • Chaînes SQL construites avec +, des templates string ou des remplacements de chaîne utilisant des valeurs de la requête
  • Texte de recherche placé directement dans LIKE '%...%'
  • ORDER BY dynamique ou noms de colonne pris depuis les paramètres de requête
  • Endpoints d'administration qui font confiance aux entrées parce qu'ils sont « internes »
  • Erreurs qui révèlent du SQL brut ou des détails de la base de données

Une règle simple tient la route : si la base de données peut interpréter l'entrée utilisateur comme des mots-clés SQL (comme OR, UNION, DROP), vous n'êtes pas réellement protégé. Si la base ne voit l'entrée utilisateur que comme des valeurs liées à des paramètres, vous allez dans la bonne direction.

Motifs vulnérables courants produits par les outils d'IA

L'injection apparaît souvent aux mêmes endroits : recherche, filtrage, tri et pagination. Ces zones paraissent être un simple traitement de chaînes, mais elles touchent la base de données presque à chaque requête.

Un motif classique est la concaténation de l'entrée utilisateur dans une clause WHERE :

// Vulnerable
const q = req.query.q;
const sql = "SELECT * FROM users WHERE email = '" + q + "'";
await db.query(sql);

Un autre motif fréquent consiste à construire des « filtres optionnels » en ajoutant des fragments bruts dans une boucle, surtout LIKE '%${q}%' et status = '${status}'. Le risque augmente dès qu'un champ est oublié ou « assaini » par un remplacement de chaîne faible.

Le tri et la pagination sont souvent des oublis. On cherche l'injection dans WHERE, puis on oublie que ORDER BY et LIMIT proviennent souvent directement des params :

// Vulnerable
const sort = req.query.sort;     // e.g. "created_at DESC"
const limit = req.query.limit;   // e.g. "50"
const sql = `SELECT * FROM orders ORDER BY ${sort} LIMIT ${limit}`;
await db.query(sql);

Vous verrez aussi des raccourcis répétés dans les prototypes :

  • Échappement fait à certains endroits mais oublié dans un endpoint
  • Autoriser des noms de colonne arbitraires pour un tri « flexible »
  • Traiter les identifiants comme sûrs parce que « ce sont des nombres », puis les utiliser comme chaînes
  • Logger du SQL brut avec l'entrée utilisateur (ce qui peut divulguer des données sensibles)
  • Copier-coller un motif de requête dangereux sur plusieurs routes

Si vous avez hérité d'une app générée par des outils comme Bolt, v0, Cursor ou Replit, supposez que ces motifs existent jusqu'à preuve du contraire.

Exemple concret : corriger du SQL brut en utilisant des paramètres

L'interpolation de chaînes a souvent l'air propre, mais elle mélange code et entrée utilisateur dans la même chaîne.

Exemple vulnérable :

// GET /users?email=...
const email = req.query.email;
const sql = `SELECT id, email FROM users WHERE email = '${email}'`;
const rows = await db.query(sql);

Si quelqu'un passe x' OR '1'='1, la requête peut renvoyer tous les utilisateurs. La correction consiste à garder le texte SQL statique et à passer les valeurs séparément.

Remplacement sûr : placeholders + valeurs séparées

Placeholders PostgreSQL :

const email = req.query.email;
const sql = "SELECT id, email FROM users WHERE email = $1";
const rows = await db.query(sql, [email]);

Placeholders MySQL/SQLite :

const sql = "SELECT id, email FROM users WHERE email = ?";
const rows = await db.query(sql, [email]);

L'idée clé est simple : l'entrée n'est pas collée dans la chaîne SQL. Le driver l'envoie comme des données, pas comme du code.

Cas limites : listes IN (...) et valeurs vides

Les filtres comme « status in [a, b, c] » sont souvent corrigés de façon incorrecte.

Unsafe :

const statuses = req.query.statuses; // e.g. "active,paused"
const sql = `SELECT * FROM users WHERE status IN (${statuses})`;

Plus sûr : construire des placeholders et passer les valeurs séparément.

const statuses = (req.query.statuses || "")
  .split(",")
  .map(s => s.trim())
  .filter(Boolean);

if (statuses.length === 0) return res.json([]); // or skip the filter

const placeholders = statuses.map((_, i) => `$${i + 1}`).join(", ");
const sql = `SELECT * FROM users WHERE status IN (${placeholders})`;
const rows = await db.query(sql, statuses);

Règles pratiques qui évitent des régressions :

  • Traitez les chaînes vides comme « pas de filtre », pas comme du texte SQL
  • Validez les types tôt (les nombres doivent être des nombres avant la requête)
  • N'acceptez jamais de fragments SQL bruts depuis la requête, même ceux qui semblent « inoffensifs »
  • Centralisez la construction des requêtes pour que les corrections tiennent

Exemple concret : filtrage sûr sans construire du SQL par concaténation

Un motif d'injection courant est les « filtres optionnels » transformés en une chaîne WHERE qui grandit.

Forme vulnérable :

// ❌ Vulnerable: string-built WHERE
let where = "WHERE 1=1";
if (q) where += ` AND name ILIKE '%${q}%'`;
if (minPrice) where += ` AND price >= ${minPrice}`;
if (startDate) where += ` AND created_at >= '${startDate}'`;

const sql = `SELECT * FROM products ${where} ORDER BY created_at DESC LIMIT ${limit}`;

Motif plus sûr : construire les conditions séparément, garder l'entrée utilisateur hors du texte SQL et passer les valeurs en paramètres.

// ✅ Safe: conditions + params
const conditions = [];
const params = [];

if (q) {
  params.push(`%${q}%`);
  conditions.push(`name ILIKE $${params.length}`);
}

if (minPrice) {
  params.push(Number(minPrice));
  conditions.push(`price >= $${params.length}`);
}

if (startDate) {
  params.push(new Date(startDate));
  conditions.push(`created_at >= $${params.length}`);
}

const whereSql = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
const sql = `SELECT * FROM products ${whereSql} ORDER BY created_at DESC LIMIT 50`;
const rows = await db.query(sql, params);

Quelques détails qui évitent des bugs subtils :

  • Mettez les wildcards % dans le paramètre, pas dans la chaîne SQL
  • Analysez et validez les dates, puis liez-les comme paramètres
  • Convertissez les plages numériques et rejetez le NaN avant le lien
  • Si un filtre est optionnel, omettez complètement la condition

Exemple concret : tri et pagination sûrs

Triage Security Issues
Si vous suspectez des secrets exposés ou des requêtes bâclées, nous trions et priorisons les corrections.

Le tri est un endroit où des endpoints autrement « paramétrés » deviennent risqués. Les valeurs comme le texte de recherche peuvent être paramétrées. Les noms de colonne et la direction de tri ne le peuvent pas.

Le motif sûr : mapper l'entrée utilisateur à une petite liste blanche de colonnes et de directions sûres, et rejeter le reste.

// Example: Node/Express with Postgres (pg)
const SORT_FIELDS = {
  createdAt: 'created_at',
  email: 'email',
  status: 'status'
};

function buildListUsersQuery({ sort = 'createdAt', dir = 'desc', page = 1, pageSize = 20 }) {
  const field = SORT_FIELDS[sort];
  if (!field) throw new Error('Invalid sort field');

  const direction = String(dir).toLowerCase() === 'asc' ? 'ASC' : 'DESC';

  const limit = Math.min(Math.max(parseInt(pageSize, 10) || 20, 1), 100);
  const offset = Math.max((parseInt(page, 10) || 1) - 1, 0) * limit;

  // Only the allowlisted identifier and direction are interpolated.
  // Pagination values stay parameterized.
  return {
    text: `SELECT id, email, status, created_at FROM users ORDER BY ${field} ${direction} LIMIT $1 OFFSET $2`,
    values: [limit, offset]
  };
}

Workflow pas à pas pour patcher une app CRUD existante

Quand une app CRUD « marche pour la plupart », l'injection se cache souvent sur les bords : recherche, filtres, tri, panneaux admin et actions en masse. Un workflow de correctif vous empêche de réparer un endpoint en laissant trois autres ouverts.

  1. Inventaire de chaque requête et chemin d'entrée. Listez l'endpoint, le type de requête (lecture/écriture), d'où viennent les entrées et quels champs atteignent la base. Incluez les jobs en arrière-plan et les outils d'administration.

  2. Remplacez le SQL construit par chaîne par des paramètres. Cherchez les concaténations de requêtes et remplacez-les par des requêtes paramétrées ou un constructeur de requêtes. Faites-le même pour les endpoints « internes ».

  3. Ajoutez des listes blanches pour les identifiants. Vous ne pouvez pas paramétrer en toute sécurité les noms de colonne, noms de table ou directions de tri. Si l'entrée utilisateur contrôle ORDER BY, les colonnes sélectionnées ou les jointures, mappez-les à des identifiants connus sûrs.

  4. Ajoutez quelques tests ciblés. Envoyez des payloads qui essaient de briser les guillemets (une simple quote), des astuces booléennes (OR 1=1) et des types inattendus. Validez le comportement : pas de lignes supplémentaires, pas de fuite de données, pas d'erreur SQL exposée.

  5. Re-vérifiez les logs et la gestion des erreurs. Provoquez volontairement des erreurs et confirmez que les réponses n'incluent pas du SQL brut, des traces de pile ou des détails du driver. Gardez des erreurs détaillées dans les logs serveur et redactez les valeurs pouvant contenir des données sensibles.

Utilisation plus sûre des ORM (et pièges à éviter)

Un ORM peut bloquer l'injection, mais seulement si vous restez sur ses chemins sûrs. Cela signifie généralement laisser l'ORM construire le SQL pendant que vous passez l'entrée utilisateur comme valeurs.

Les patterns « sûrs » ressemblent à « filtrer par ces champs », pas à « construire une chaîne SQL ».

// Example: safe parameter binding (generic)
const users = await db.user.findMany({
  where: {
    email: inputEmail,   // value is bound, not concatenated
    isActive: true
  },
  take: 25
});

// Example: query builder style with placeholders
const users2 = await knex('users')
  .where('email', '=', inputEmail)
  .andWhere('is_active', '=', true)
  .limit(25);

Les ORM ont encore des sorties « dangereuses », et le code généré par l'IA a souvent tendance à les surutiliser. Méfiez-vous de :

  • Helpers de requêtes brutes avec interpolation de chaînes
  • Méthodes explicitement marquées « unsafe »
  • Passage d'entrée utilisateur dans des identifiants (noms de colonnes, noms de tables)
  • API qui acceptent un fragment SQL au lieu d'une valeur

Si vous devez utiliser du SQL brut, utilisez la fonctionnalité de paramètres de l'ORM, pas les template strings. Pour des noms de colonnes dynamiques, utilisez une whitelist.

Erreurs courantes lors du patch d'une injection SQL

Audit Every Query Path
Nous passons en revue les recherches, filtres, tris et routes admin où l'injection SQL se cache généralement.

La validation d'entrée ne suffit pas. Bloquer quelques caractères ou enlever des espaces peut réduire le bruit, mais n'empêche pas l'injection si une valeur finit toujours dans une chaîne SQL.

L'échappement manuel est un autre piège. On a l'impression d'être plus sûr en remplaçant des quotes, mais les règles d'échappement varient selon les bases et des cas-limites sont faciles à manquer. Les requêtes paramétrées sont plus sûres et plus simples à auditer.

Les prepared statements peuvent aussi être mal utilisés. Une erreur fréquente est de paramétrer la valeur de recherche mais de concaténer encore des morceaux SQL comme ORDER BY :

const sql = `SELECT * FROM users WHERE name ILIKE $1 ORDER BY ${sort} ${dir}`;
await db.query(sql, [`%${q}%`]);

Si sort ou dir vient de la requête, un attaquant peut s'en sortir. Corrigez avec une whitelist stricte pour les identifiants.

Le logging est aussi un risque sous-estimé. Logger le SQL complet avec l'entrée utilisateur peut divulguer des emails, tokens ou autres secrets copiés dans une recherche. Gardez les logs au niveau macro et masquez les champs sensibles.

Exemple réaliste : endpoint de recherche admin vulnérable

Un fondateur déploie en urgence un panneau admin généré par l'IA. Il propose une simple recherche : « Search users by email or name ». Le backend construit une chaîne SQL depuis ce que l'admin tape, puis l'exécute tel quel.

Cela paraît anodin parce que c'est « réservé aux admins ». Mais les endpoints admin sont exposés dans la vraie vie : une route mal configurée, un cookie fuit, un mot de passe faible ou un outil interne déployé accidentellement sur une URL publique.

L'injection arrive quand l'entrée de recherche est placée dans une requête comme :

SELECT id, email, role FROM users WHERE email LIKE '%{q}%' OR name LIKE '%{q}%'

Un payload réaliste ferme la quote et ajoute une condition vraie, comme %' OR 1=1 --. Maintenant la clause WHERE est toujours vraie, et la réponse peut déverser bien plus de données que prévu.

Dans une version patchée, la même recherche utilise des paramètres. Si quelqu'un envoie %' OR 1=1 --, c'est traité comme du texte brut et se comporte comme une recherche normale (généralement sans résultat dangereux).

Checklist rapide avant mise en production

Make It Production Ready
Nous refactorons les motifs risqués et préparons l'application au déploiement sans correctifs fragiles.

Avant de considérer le travail terminé, faites un passage rapide sur les endroits où l'injection se cache encore.

  • Cherchez dans le code tout texte SQL proche de valeurs issues de la requête. Si vous voyez de la concaténation ou des template strings, considérez-le comme non sûr jusqu'à preuve du contraire.
  • Reverifiez chaque clause dynamique, pas seulement WHERE. ORDER BY, LIMIT/OFFSET et IN (...) sont des oublis fréquents.
  • Confirmez que chaque valeur contrôlée par l'utilisateur est liée comme paramètre (y compris les entrées numériques comme les IDs et tailles de page).
  • Assurez-vous que les erreurs ne fuient pas le texte SQL ou les traces de pile.
  • Probe les routes à haut risque (login, recherche, filtres admin) avec quelques payloads basiques et vérifiez que le comportement reste inerte.

Prochaines étapes : rendre le chemin sûr par défaut

La vraie victoire n'est pas de patcher une seule requête. C'est rendre l'injection difficile à réintroduire la prochaine fois qu'un outil IA génère un endpoint CRUD « utile ».

Une règle simple qui évite la plupart des régressions : pas de SQL brut construit à partir de chaînes de requête. Si vous avez besoin d'un tri dynamique ou d'une sélection de champs, utilisez des whitelists strictes.

Si vous avez hérité d'une base de code générée par l'IA et voulez une revue rapide et structurée, FixMyMess (fixmymess.ai) se concentre sur le diagnostic et la réparation de ces problèmes typiques de prototypes, y compris les motifs de requêtes dangereux, les brèches d'authentification et le renforcement de la sécurité, avant qu'ils n'atteignent la production.