Aller au contenu principal
own2pwn
appsec/attaques-par-injection-sql.tsx

Attaques par injection SQL : anatomie, familles et défenses qui tiennent

Les attaques par injection SQL, contexte par contexte : chaîne, numérique, identifiant, ORDER BY. Les cinq familles dont l'injection SQL aveugle, ce qu'une WAF change vraiment, et la seule parade solide, les requêtes préparées.

own2pwn14 min de lecture

Combien de concaténations dorment dans votre code ?

Un pentest en boîte blanche remonte chaque point où une saisie touche une requête SQL.

Faire auditer mon code

L'injection SQL a été décrite publiquement en décembre 1998, dans le numéro 54 de Phrack, par rain.forest.puppy. Près de trente ans plus tard, CWE-89 occupait encore la troisième place du CWE Top 25 2024 du MITRE, et la campagne d'extorsion la plus massive de 2023 est partie de là : CVE-2023-34362, une injection SQL dans MOVEit Transfer, exploitée en masse avant même l'existence d'un correctif, au point que la CISA et le FBI publiaient une alerte conjointe le 7 juin 2023. Les attaques par injection SQL forment une classe de vulnérabilité vieille de trois décennies, enseignée dans tous les cursus, et qui vide encore des bases entières.

Les développeurs connaissent le problème. C'est la mécanique qui ne désarme pas : écrire une requête SQL en collant des morceaux de texte reste la chose la plus naturelle du monde dans n'importe quel langage, et il suffit d'un seul endroit oublié, souvent un filtre de tri ou un export CSV écrit un vendredi soir, pour rouvrir la porte. Dans les attaques par injection SQL, le contexte décide de la difficulté, la famille décide du débit d'extraction, et la défense tient en une ligne de code que deux réglages de pilote suffisent à rendre cosmétique.

Registre défensif

Tout ce qui suit sert à comprendre et corriger. Les extraits de code sont des exemples de correction, pas des charges prêtes à l'emploi, et tester une base de données qui ne vous appartient pas, sans mandat écrit, reste une infraction pénale, quel que soit l'intérêt de la démarche.

Ce qu'une injection SQL fait à la base de données

Le mot "injection" décrit mal ce qui se passe. Rien n'est injecté dans un tuyau : l'application fabrique une chaîne de caractères, la base la reçoit, et son analyseur syntaxique la découpe en mots-clés, identifiants, opérateurs et littéraux. Ce découpage est la seule chose qui compte. Si la saisie de l'utilisateur se retrouve à cheval sur la frontière entre un littéral et de la structure, alors cette saisie décide de la forme de la requête, et plus seulement de son contenu.

anatomie
Client
Une saisie quelconque
Un champ de formulaire, un paramètre d'URL, un en-tête, un champ JSON, un nom de fichier importé.
Application
Construction de la requête par concaténation
Le texte du développeur et le texte du client sont fondus en une seule chaîne, indiscernables l'un de l'autre.
Base de données
Analyse syntaxique
Le parseur découpe la chaîne. Il n'a aucun moyen de savoir quelle partie venait d'internet.
Base de données
Plan d'exécution modifié
Une clause a été ajoutée, un littéral refermé, une jointure greffée. Le moteur exécute fidèlement ce qu'on lui a demandé.
La faille ne naît pas dans la base : elle naît au moment où l'application décide que du texte reçu du client fait partie de sa requête.

Ce détour par le parseur explique pourquoi les défenses par filtrage vieillissent si mal. Un filtre travaille sur du texte HTTP ; la vulnérabilité, elle, vit dans la structure de l'arbre syntaxique que la base construira ensuite. Les deux ne parlent pas le même langage, et c'est tout le sujet.

Le contexte d'injection décide de tout

La première question qu'on se pose devant un point d'entrée suspect n'est pas "est-ce vulnérable", c'est "où atterrit la saisie dans la requête". Ce contexte conditionne tout le reste : la difficulté, la signature laissée dans les logs, et parfois l'existence même d'une parade paramétrable.

sql
-- 1. Contexte chaîne : la valeur est encadrée par des quotes.
--    L'enjeu pour l'attaquant est de sortir du littéral.
SELECT id, email FROM users WHERE email = '<saisie>'

-- 2. Contexte numérique : aucune quote autour de la valeur.
--    Le plus permissif : il n'y a même pas de littéral à refermer.
SELECT * FROM orders WHERE customer_id = <saisie>

-- 3. Contexte identifiant : la saisie désigne une table ou une colonne.
--    Aucun marqueur de paramètre ne peut lier ça (voir plus bas).
SELECT * FROM <saisie> WHERE tenant_id = ?

-- 4. Contexte ORDER BY : la base attend une expression, pas une valeur.
--    Le grand classique des tableaux triables.
SELECT * FROM products ORDER BY <saisie> DESC

-- 5. Contexte LIKE : la valeur reste une valeur, mais % et _ sont
--    des jokers. Un paramètre lié protège de l'injection, pas du
--    balayage intégral de la table par un '%' isolé.
SELECT * FROM products WHERE name LIKE '%<saisie>%'

Les cas 1 et 2 sont ceux que tout le monde connaît, et les seuls que les requêtes préparées règlent d'un trait de plume. Le second mérite une mention particulière : on le croit protégé parce que le paramètre est "un entier", alors qu'il est le plus exposé de tous, puisque la saisie s'insère directement au niveau de la structure sans avoir à refermer quoi que ce soit. Les cas 3 et 4, eux, sont les vrais pièges de terrain, ceux qu'on retrouve encore dans des applications par ailleurs propres, et ils ont droit à leur section plus bas. Quant au dernier, il rappelle qu'une requête paramétrée peut rester une mauvaise requête : le paramètre lié empêche l'injection, pas le joker isolé qui déclenche un balayage complet sur dix millions de lignes.

Le point d'entrée n'est pas toujours un formulaire

Une injection SQL peut partir d'un cookie, d'un en-tête User-Agent journalisé en base, d'un champ d'un fichier importé, ou d'une valeur écrite par un autre service. Le cas dit de second ordre est le plus vicieux : la donnée est stockée proprement via une requête préparée, puis relue et concaténée des semaines plus tard par un traitement de nuit ou un export, loin du code qui la validait.

Les cinq familles d'attaques par injection SQL

Toutes les injections partagent la même cause. Ce qui les distingue, c'est le canal de retour, c'est-à-dire la façon dont l'attaquant récupère le résultat de ce qu'il a fait exécuter. Cette variable détermine à elle seule le temps qu'il faut pour extraire une table.

  • In-band, par UNION : la page affiche des données issues de la requête, l'attaquant greffe donc un second jeu de résultats sur le premier. Le canal est direct et le débit maximal : une table entière peut sortir en quelques requêtes. C'est la famille la plus documentée et, en 2026, la plus rare, parce qu'elle suppose que l'application recrache telle quelle une liste de colonnes.
  • Error-based : la structure n'est pas affichée, mais le message d'erreur du SGBD remonte jusqu'à la page. Les moteurs sont bavards, et un message de conversion de type peut contenir la valeur qui a provoqué l'erreur. Le débit reste bon. La contre-mesure est connue depuis longtemps, ce qui explique le déclin de la famille : erreurs génériques côté utilisateur, détail dans les logs serveur uniquement.
  • Aveugle booléenne : ni données ni erreurs, mais la réponse diffère selon que la condition injectée est vraie ou fausse. Une longueur de page, un code HTTP, la présence d'un bloc dans le HTML : n'importe quel signal binaire suffit. On ne lit plus une valeur, on la devine par questions fermées successives.
  • Aveugle par temporisation : même principe, mais le signal binaire est le temps de réponse. Quand plus aucune différence n'apparaît dans le contenu, on conditionne une fonction de temporisation (SLEEP sur MySQL, pg_sleep sur PostgreSQL, WAITFOR DELAY sur SQL Server) à la valeur testée. La réponse met deux secondes de plus, la condition était vraie.
  • Out-of-band : la base elle-même émet une connexion sortante, typiquement une résolution DNS, vers un nom contrôlé par le testeur. Le canal quitte alors complètement la session HTTP. C'est le dernier recours quand même le temps de réponse est trop bruité, et cela suppose une base autorisée à sortir du réseau, ce qui en dit long sur la segmentation. On a détaillé la mécanique de ce canal, côté infrastructure, dans construire son serveur out-of-band pour le pentest.

Ces familles ne s'excluent pas : on commence presque toujours par la plus rapide disponible et on descend l'échelle jusqu'à trouver un canal qui parle. La descente elle-même est une information de sécurité utile pour la défense, puisqu'elle indique quelles contre-mesures tiennent déjà.

Pourquoi l'injection SQL aveugle domine en 2026

Trois évolutions parfaitement saines ont, sans le vouloir, fait de l'aveugle la forme ordinaire de l'injection SQL moderne. Les pages d'erreur génériques sont devenues la norme et ont asséché la famille error-based. Les interfaces se sont déportées vers des API JSON qui renvoient des structures stables, où une requête cassée produit un 500 uniforme plutôt qu'une trace. Et les ORM ont normalisé le rendu, si bien que les rares points de concaténation survivants se situent dans du code annexe, un tri, un filtre avancé, une recherche multicritère, dont le résultat n'est pas réinjecté tel quel dans la page.

Résultat : il ne reste souvent qu'un bit d'information par requête. Extraire une chaîne se fait alors caractère par caractère, en dichotomie sur la valeur de chaque octet, soit environ sept requêtes par caractère. Un condensat de mot de passe de trente-deux caractères demande donc de l'ordre de deux cent vingt requêtes, une table de dix mille lignes se compte en centaines de milliers. C'est lent et bruyant, et c'est exactement pour cette raison que cette extraction est toujours automatisée.

canal-de-retour.txt
Le même défaut, cinq débits d'extraction. Fermer les canaux bavards ne corrige rien : cela déplace l'attaquant vers le canal du dessous.

Cette lenteur est aussi la meilleure nouvelle du tableau pour la défense. Une extraction aveugle, c'est des milliers de requêtes quasi identiques sur le même point d'entrée, avec une distribution de temps de réponse anormale. Un SGBD qui journalise ses requêtes lentes et une alerte sur la répétition d'un même motif suffisent à voir passer l'attaque, à condition que quelqu'un regarde. C'est précisément le sujet de la catégorie Logging and Alerting Failures du classement détaillé dans l'OWASP Top 10, où l'injection figure toujours, désormais au cinquième rang.

Ce qu'une WAF change, et ce qu'elle ne change pas

Une WAF inspecte une requête HTTP et cherche des motifs qui ressemblent à du SQL hostile. Elle rend deux services réels, et il serait malhonnête de les nier. Elle élimine le bruit de fond des scanners automatisés, ceux qui envoient les mêmes chaînes à tous les paramètres de la planète. Et elle offre un virtual patching qui compte, le jour où une vulnérabilité est publiée dans un composant tiers et où le correctif éditeur n'arrive pas avant plusieurs jours. Dans ce créneau précis, une règle bien écrite vaut mieux qu'une prière.

Le problème est structurel et tient en une phrase : la WAF raisonne sur une chaîne de caractères, la base raisonne sur un arbre syntaxique. Entre les deux, il existe une infinité de façons d'écrire la même requête, et chaque couche traversée en ajoute encore. Le décodage d'URL, le jeu de caractères déclaré, les commentaires SQL, la casse, les espaces significatifs ou non, les variantes de syntaxe propres à chaque moteur, la façon dont le serveur applicatif fusionne des paramètres répétés : tout cela change le texte sans changer l'arbre. La règle poursuit donc une cible qui se déforme, ce qui la condamne à courir derrière.

Le réglage qui casse la production

Une règle trop stricte finit toujours par bloquer un client qui s'appelle O'Connor, un commentaire contenant --, ou un article de blog qui parle de SQL. La WAF passe alors en mode journalisation seule "le temps de régler", et y reste. Une WAF qu'on n'ose plus mettre en blocage n'est plus une protection, c'est une ligne de plus dans un tableau de conformité.

La seule défense qui tient : les requêtes préparées

Une requête préparée avec paramètres liés rend l'injection structurellement impossible, sans filtrer ni échapper quoi que ce soit : elle sépare physiquement le transport de la structure et celui des données. La requête part avec des marqueurs, la base compile son plan d'exécution, et les valeurs arrivent ensuite, typées, dans un message distinct. Quand une valeur arrive, la forme de la requête est déjà figée : aucun contenu, si créatif soit-il, ne peut plus la modifier.

Ce luxe est plus rare qu'il n'y paraît, et on ne s'en aperçoit qu'en le cherchant ailleurs. Un modèle de langage, lui, reçoit les consignes du développeur et le contenu récupéré dans une seule séquence de tokens, sans champ de protocole pour les séparer : c'est toute la raison pour laquelle la prompt injection se contient au lieu de se corriger, là où l'injection SQL se ferme définitivement.

concat-vs-prepare.txt
À gauche, structure et données arrivent mélangées et c'est le parseur qui tranche. À droite, la structure est compilée avant que la moindre donnée utilisateur n'existe.

Le contre-exemple canonique, celui qu'on retrouve dans toutes les bases de code anciennes, tient en deux lignes. La correction aussi.

php
<?php
// VULNÉRABLE : la saisie devient une portion de la requête.
// Peu importe la fonction d'échappement ajoutée ensuite, le
// problème est déjà là : c'est PHP qui fabrique le SQL final.
$sql = "SELECT id, email, role FROM users WHERE email = '" . $_POST['email'] . "'";
$user = $pdo->query($sql)->fetch();

// CORRIGÉ : la requête part avec un marqueur, la valeur suit.
$stmt = $pdo->prepare('SELECT id, email, role FROM users WHERE email = ?');
$stmt->execute([$_POST['email']]);
$user = $stmt->fetch();

Deux détails de configuration décident si cette correction est réelle ou cosmétique. Par défaut, certains pilotes émulent la préparation côté client : ils interpolent eux-mêmes les valeurs dans la chaîne avant de l'envoyer, et on se retrouve à faire confiance à leur routine d'échappement plutôt qu'à la séparation des canaux. En PDO, cette émulation se désactive explicitement.

php
<?php
$pdo = new PDO($dsn, $dbUser, $dbPassword, [
    // Vraies requêtes préparées côté serveur, pas d'interpolation locale.
    PDO::ATTR_EMULATE_PREPARES   => false,
    // Les erreurs remontent en exception, elles ne partent pas dans la page.
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);

Le même piège existe dans les autres écosystèmes, sous une forme plus subtile encore, parce que la syntaxe correcte ressemble à s'y méprendre à du formatage de chaîne. En Python, le %s d'un pilote de base de données n'est pas un opérateur de formatage : c'est un marqueur de paramètre, et il ne doit jamais être appliqué par Python lui-même.

python
# VULNÉRABLE : la f-string fabrique la requête avant que la base la voie.
cur.execute(f"SELECT id, amount FROM orders WHERE customer_id = {customer_id}")

# VULNÉRABLE AUSSI, et c'est le piège : l'opérateur % de Python.
cur.execute("SELECT id, amount FROM orders WHERE customer_id = %s" % customer_id)

# CORRECT : le %s reste dans la chaîne, la valeur voyage à part.
cur.execute("SELECT id, amount FROM orders WHERE customer_id = %s", (customer_id,))

# Une liste de longueur variable : un marqueur par élément, générés par
# le code, jamais à partir du texte reçu du client.
placeholders = ", ".join(["%s"] * len(ids))
cur.execute(f"SELECT id FROM orders WHERE id IN ({placeholders})", tuple(ids))

Les faux amis qui donnent l'illusion d'être protégé

Quatre pratiques reviennent systématiquement dans les revues de code et donnent toutes le sentiment du devoir accompli. Aucune ne remplace un paramètre lié. L'échappement manuel d'abord, qui consiste à doubler les quotes soi-même : il dépend du jeu de caractères de la connexion, il ignore le contexte numérique, et il ne survit pas au premier développeur qui ajoutera un champ sans y penser. Les listes de mots interdits ensuite, qui rejettent UNION ou SELECT : elles bloquent surtout les utilisateurs légitimes et se contournent par la syntaxe.

Viennent ensuite les procédures stockées, réputées sûres par principe, alors qu'une procédure qui construit une requête dynamique en concaténant ses propres paramètres est aussi vulnérable que le code applicatif qu'elle remplace, avec en prime des privilèges souvent plus élevés. Et enfin l'ORM, qui protège effectivement la quasi-totalité du code généré, mais laisse ouvertes ses portes de sortie : SQL brut, fragments d'expression libres, clause de tri construite depuis un paramètre HTTP. L'ironie est connue : plus l'ORM couvre le code, moins les revues regardent les rares lignes qu'il ne couvre pas.

Ce qu'on ne peut pas paramétrer, et comment on le traite

Un marqueur de paramètre remplace une valeur, rien d'autre. Un nom de table, un nom de colonne, un sens de tri, un mot-clé SQL, une clause entière : aucun de ces éléments n'est liable, et pour une raison qui n'a rien d'arbitraire. Ils font partie de la structure, donc de ce que la base doit connaître pour compiler son plan, c'est-à-dire avant que le moindre paramètre n'arrive. Écrire ORDER BY ? ne produit pas une erreur d'injection : cela produit un tri par une constante, silencieux et faux.

La parade tient en un principe : le texte du client n'atteint jamais la requête, quel que soit le soin mis à l'échapper. On passe par une table de correspondance écrite en dur, dont les clés sont ce que le client a le droit de demander et les valeurs sont les identifiants SQL réels. Tout ce qui n'y figure pas est refusé, et jamais "corrigé" en douce.

python
# Allowlist : la seule chose qui vient du client est une CLÉ de dictionnaire.
# Les identifiants SQL, eux, sont des littéraux écrits ici, dans le code.
SORTABLE = {
    "date":    "created_at",
    "montant": "amount_cents",
    "client":  "customer_name",
}
DIRECTIONS = {"asc": "ASC", "desc": "DESC"}

sort_key  = request.args.get("tri", "date")
direction = request.args.get("sens", "desc")

column = SORTABLE.get(sort_key)
order  = DIRECTIONS.get(direction.lower())
if column is None or order is None:
    abort(400)   # on refuse, on ne devine pas une valeur proche

# Cette f-string est sûre : les deux variables interpolées ne peuvent
# valoir que l'une des chaînes écrites ci-dessus. La valeur métier,
# elle, reste un paramètre lié.
cur.execute(
    f"SELECT id, amount_cents FROM orders "
    f"WHERE tenant_id = %s ORDER BY {column} {order} LIMIT 100",
    (tenant_id,),
)

Trois points font la différence entre cette allowlist et un filtre déguisé. Le dictionnaire est fermé : on ne le construit pas à partir des métadonnées de la base, sinon un nom de colonne inattendu redevient une entrée libre. L'échec est net : on renvoie une erreur, on ne retombe pas silencieusement sur une valeur par défaut qui masquerait une tentative. Et la séparation reste visible dans le code : les identifiants sont interpolés, la valeur métier passe par un paramètre lié, si bien qu'une relecture voit immédiatement ce qui vient d'où.

Le cas des noms d'objets vraiment dynamiques

Un outil multi-tenant qui doit viser une table par client, ou un back-office d'administration de schéma, ne peut pas énumérer ses identifiants à l'avance. Il reste alors la fonction de citation d'identifiant fournie par le pilote, jamais une concaténation maison, doublée d'une vérification que l'objet visé existe et appartient bien au tenant courant. C'est un cas rare et il se traite en connaissance de cause, pas par réflexe.

Les couches qui limitent la casse quand une injection passe

Aucune de ces couches ne corrige une injection SQL. Toutes changent son résultat, et c'est la différence entre un incident et une notification de violation de données à la CNIL. Le compte de base de données utilisé par l'application illustre bien le sujet : une injection sur un compte limité en lecture à trois tables reste une injection, mais elle ne lit pas la table des mots de passe, ne crée pas de fichier sur le serveur et n'appelle pas de procédure étendue.

defense-en-profondeur
Fondation
Requêtes préparées
Séparation structure / données. La seule couche qui supprime la vulnérabilité.
Périmètre de droits
Compte SQL au moindre privilège
Un compte par usage, pas de DBA applicatif, pas de droit sur les tables système.
Discrétion
Erreurs génériques
Message neutre côté client, trace complète côté serveur. Tue la famille error-based.
Réseau
Base sans accès sortant
Coupe le canal out-of-band et l'exfiltration par DNS.
Détection
Journalisation et alerte
Requêtes lentes, motifs répétés, pics de latence sur un même paramètre.
Une seule couche corrige la faille. Les autres décident de ce que l'attaquant obtient le jour où elle manque quelque part.

Vérifier qu'une application n'est plus injectable

Trois approches se complètent, et chacune laisse un angle mort que les deux autres couvrent. L'analyse statique remonte les points de concaténation en suivant le flux depuis les sources d'entrée jusqu'aux appels SQL : c'est la seule méthode qui voit le code mort, la branche rarement empruntée et le traitement de nuit. Elle produit aussi des faux positifs en série dès qu'une valeur passe par une fonction qu'elle ne sait pas modéliser. L'analyse dynamique attaque l'application en cours d'exécution et confirme l'exploitabilité réelle, y compris sur les cas aveugles où seul le temps de réponse parle ; c'est le domaine du test dynamique de sécurité applicative. La comparaison détaillée des deux familles, et ce que l'IA y change, est traitée dans notre article de fond sur le comparatif SAST, DAST et IAST.

Ces deux outils ne valent que branchés au bon endroit du pipeline, l'analyse statique sur la pull request et l'analyse dynamique sur un environnement de recette, faute de quoi leurs résultats finissent dans un rapport que personne n'ouvre ; c'est tout l'objet de l'intégration de SAST et DAST dans la CI/CD. Reste la troisième approche, le test manuel, qui seul traite les cas de second ordre, les injections derrière une logique métier à plusieurs étapes et les points d'entrée qu'aucun crawler ne trouve parce qu'ils supposent de comprendre le fonctionnement du produit. Le déroulement d'un test d'intrusion web détaille la place que cette recherche occupe dans une mission complète.

Pour la référence normative, la SQL Injection Prevention Cheat Sheet de l'OWASP fait autorité et couvre les cas particuliers par langage ; le module sur l'injection SQL aveugle de PortSwigger reste le meilleur support pédagogique gratuit pour manipuler le sujet dans un lab prévu pour ça.

À retenir

  • Une injection SQL naît dans l'application, pas dans la base : au moment où du texte reçu du client est fondu dans la requête, plus rien ne distingue la structure des données.
  • Le contexte d'injection commande tout. Le contexte numérique est le plus permissif ; les contextes identifiant et ORDER BY sont les seuls qu'un paramètre lié ne peut pas couvrir.
  • Les cinq familles se distinguent par leur canal de retour. En 2026, les erreurs génériques et les API JSON ont fait de l'aveugle, booléenne ou par temporisation, le cas ordinaire : lent, bruyant, donc détectable.
  • Une WAF rend deux vrais services, filtrer les scanners et tenir en attendant un correctif éditeur. Elle raisonne sur une chaîne quand la base raisonne sur un arbre syntaxique, ce qui la laisse toujours en retard d'une réécriture.
  • Les requêtes préparées avec paramètres liés sont la seule défense structurelle. Vérifier que l'émulation côté client est désactivée, et se méfier des faux amis : échappement manuel, listes de mots interdits, procédures stockées dynamiques, SQL brut d'ORM.
  • Ce qui ne se paramètre pas se traite par allowlist fermée, avec un refus net en cas d'entrée inconnue. Autour, le moindre privilège et l'absence d'accès sortant décident de l'ampleur des dégâts.

Dix ans d'historique dans un dépôt, et il reste des concaténations quelque part. Le travail consiste à trouver lesquelles sont atteignables depuis internet, ce que cherche exactement un pentest web en boîte blanche, code et requêtes sous les yeux ; entre deux missions, la plateforme AppSec augmentée par l'IA prend le relais sur le code qui bouge. Un périmètre précis à faire regarder ? La page contact suffit.

Questions fréquentes

Qu'est-ce qu'une attaque par injection SQL ?

Une attaque par injection SQL consiste à faire passer une saisie utilisateur pour une partie de la requête envoyée à la base de données. L'application construit sa requête en collant du texte, l'attaquant fournit une valeur qui referme un littéral ou ajoute une clause, et le moteur SQL exécute une structure que le développeur n'avait pas écrite. Les conséquences vont de la lecture de tables entières à la modification de données, parfois à l'exécution de commandes système selon les privilèges du compte de base.

Comment fonctionne une injection SQL aveugle ?

L'injection SQL aveugle s'applique quand la page ne renvoie ni données ni message d'erreur exploitable. L'attaquant transforme alors chaque question en question fermée à laquelle l'application répond sans le vouloir : une différence de contenu ou de code HTTP entre une condition vraie et une condition fausse (aveugle booléenne), ou un temps de réponse allongé par une fonction de temporisation quand la condition est vraie (aveugle par temporisation). Une donnée s'extrait ainsi bit par bit, ce qui demande des milliers de requêtes mais fonctionne sans aucun retour visible.

Comment se protéger de l'injection SQL ?

Par des requêtes préparées avec paramètres liés, systématiquement. La requête part avec des marqueurs à la place des valeurs, la base compile son plan avant de recevoir la moindre donnée utilisateur, et cette donnée ne peut plus devenir de la structure. L'échappement manuel, les listes de mots interdits et les filtres d'entrée sont des rustines qui cèdent sur un encodage ou un jeu de caractères inattendu. Autour de cette base : compte de base de données au moindre privilège, messages d'erreur génériques, et vérification par test régulier.

Un ORM protège-t-il automatiquement des injections SQL ?

Un ORM utilisé normalement génère des requêtes paramétrées, donc oui pour l'écrasante majorité du code. Le risque se concentre dans les échappatoires : SQL brut, fragments passés à un filtre libre, clause ORDER BY construite depuis un paramètre HTTP, condition assemblée en chaîne avant d'être remise à l'ORM. Ces quelques lignes suffisent, et ce sont précisément celles que les revues survolent parce que le reste du fichier semble sûr.

Une WAF suffit-elle à bloquer les attaques par injection SQL ?

Non. Une WAF inspecte une chaîne HTTP, pas l'arbre syntaxique que la base finira par exécuter ; elle raisonne sur des motifs, alors que la vulnérabilité vit dans la façon dont l'application assemble sa requête. Elle arrête les scanners génériques et fait gagner du temps sur une vulnérabilité connue en attendant le correctif, ce qui n'est pas rien. Elle ne remplace pas une requête préparée, et une règle trop stricte casse les saisies légitimes contenant une apostrophe.

Que faire quand on ne peut pas paramétrer, par exemple un nom de colonne ?

Les marqueurs de paramètre ne remplacent que des valeurs : un nom de table, un nom de colonne, un sens de tri ou un mot-clé SQL ne peuvent pas être liés. Le traitement correct est une allowlist : une table de correspondance écrite dans le code qui associe l'entrée utilisateur à un identifiant littéral, et un refus net si l'entrée n'y figure pas. La chaîne finale ne contient alors que des valeurs choisies par le développeur, jamais du texte venu du client.

Veille sécurité

La suite, une fois par mois

Ce qui bouge vraiment sur la surface d'attaque externe, NIS2 et la sécurité applicative, écrit par le pentester qui signe ces articles. Un envoi par mois, désinscription en un clic.

Votre adresse ne sert qu'à cet envoi. Voir la politique de confidentialité.

Articles liés