aslain.dev
0%
01 Hizmetler 02 Hakkımda 03 Projeler 04 Stack 05 Blog 06 İletişim
← Tüm makaleler Développement Web

Injection SQL : comprendre et prévenir l'attaque

L'injection SQL (SQL injection) est l'une des attaques les plus anciennes et les plus dévastatrices contre les applications web ; elle survient lorsque des données venant de l'utilisateur sont mélangées directement à une requête SQL. En envoyant un texte qui modifie le sens de la requête au lieu d'une valeur normale, un attaquant peut lire des données, les modifier, voire supprimer des tables entières. La bonne nouvelle : avec les bons schémas, on peut fermer cette classe d'attaque presque complètement. Dans cet article, nous verrons comment elle fonctionne à partir d'exemples réels et nous construirons, étape par étape, la principale ligne de défense — les requêtes préparées (requêtes paramétrées).

Comment fonctionne l'injection SQL ?

Le problème de fond est que le code réunit la donnée et la commande dans une même chaîne. La base de données interprète l'intégralité du texte qu'elle reçoit ; elle n'a aucun moyen de savoir quelle partie est du « code de confiance » et quelle partie est une « donnée utilisateur ». Un exemple classique et défaillant :

// NE JAMAIS FAIRE CECI
$email = $_POST['email'];
$sql = "SELECT * FROM users WHERE email = '$email'";
$result = $db->query($sql);

Si l'utilisateur saisit une adresse normale, aucun souci. Mais si l'entrée est ' OR '1'='1, la requête devient :

SELECT * FROM users WHERE email = '' OR '1'='1'

Comme '1'='1' est toujours vrai, la requête renvoie tous les utilisateurs. Sur un écran de connexion, cela signifie se connecter sans mot de passe.

Exemples d'attaques réelles

L'injection SQL ne prend pas une seule forme. Reconnaître les types les plus courants explique pourquoi de simples astuces comme « supprimer les guillemets » sont insuffisantes.

  • Contournement d'authentification : le schéma OR '1'='1 ci-dessus est l'exemple le plus connu sur les formulaires de connexion.
  • Basée sur UNION : l'attaquant ajoute son propre résultat SELECT à la requête existante. Par exemple, ' UNION SELECT username, password FROM users -- peut exfiltrer des données d'une autre table.
  • Basée sur les erreurs : les messages d'erreur détaillés renvoyés par la base exposent la structure des tables et des colonnes ; l'attaquant s'en sert pour cartographier le schéma.
  • Injection à l'aveugle (blind) : même si l'application n'affiche aucune erreur, les données peuvent être extraites bit par bit en observant les réponses vrai/faux ou le temps de réponse (comme AND SLEEP(5)).

Un point important : tenter de « nettoyer » l'entrée en retirant les guillemets est une défense fragile. Chaque base a ses propres règles d'échappement et astuces d'encodage ; tôt ou tard, une faille subsiste. La bonne solution consiste à séparer entièrement la donnée du code.

La défense principale : les requêtes préparées

Une requête préparée (requête paramétrée) sépare la structure de la requête de ses données. Vous préparez d'abord le modèle de requête avec des marqueurs, puis vous envoyez les valeurs par un canal distinct. Quelle que soit la valeur, la base la traite comme une simple valeur, jamais comme une commande. En PHP avec PDO :

$stmt = $pdo->prepare('SELECT id, name FROM users WHERE email = ?');
$stmt->execute([$email]);
$user = $stmt->fetch();

Ici, ? est un marqueur. La valeur passée à execute() est insérée après que la structure de la requête est déjà verrouillée ; qu'elle contienne un guillemet, un OR ou un -- ne change rien. Les paramètres nommés améliorent la lisibilité :

$stmt = $pdo->prepare(
    'INSERT INTO posts (title, user_id) VALUES (:title, :user_id)'
);
$stmt->execute([':title' => $title, ':user_id' => $userId]);

Si vous utilisez PDO, désactivez l'émulation pour garantir que le pilote utilise de vraies requêtes préparées :

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

Le même principe vaut dans tous les langages. En Node.js avec mysql2 :

const [rows] = await conn.execute(
  'SELECT id, name FROM users WHERE email = ?',
  [email]
);

Une seule règle clé : ne jamais coller des données utilisateur dans une requête par concaténation de chaînes. Toujours les passer comme paramètre.

ORM et constructeurs de requêtes

Les frameworks modernes génèrent automatiquement des requêtes paramétrées lorsqu'ils sont bien utilisés. Dans Laravel, Eloquent et le constructeur de requêtes lient les valeurs comme paramètres d'eux-mêmes :

// Sûr — la valeur est liée automatiquement
User::where('email', $email)->first();

DB::table('users')->where('email', $email)->get();

Mais un ORM ne vous protège pas automatiquement ; dès que vous écrivez du SQL brut, la responsabilité vous revient. Si vous placez des données utilisateur directement dans DB::raw() ou dans un argument whereRaw(), la faille réapparaît. Même là, utilisez des liaisons :

// Faux : valeur intégrée directement
DB::select("SELECT * FROM users WHERE email = '$email'");

// Correct : tableau de liaisons
DB::select('SELECT * FROM users WHERE email = ?', [$email]);

Comment sécuriser les noms de table et de colonne ?

Les marqueurs ne fonctionnent que pour les valeurs ; les parties structurelles comme un nom de table, un nom de colonne ou un sens d'ORDER BY ne peuvent pas être des paramètres. ORDER BY ? ne fonctionnera pas. Si un utilisateur peut choisir la colonne de tri, ne placez pas la valeur reçue directement dans la requête — validez-la par rapport à une liste blanche fixe :

$allowed = ['name', 'created_at', 'price'];
$column = in_array($_GET['sort'], $allowed, true)
    ? $_GET['sort']
    : 'name';

$sql = "SELECT * FROM products ORDER BY $column";

Ici, l'utilisateur ne peut choisir que l'une des valeurs préapprouvées ; aucun autre texte ne peut atteindre la requête.

Défense en profondeur

Les requêtes préparées constituent la défense principale et suffisante contre l'injection SQL. Mais la sécurité se pense de façon globale ; ces couches réduisent la surface d'attaque et limitent les dégâts potentiels :

  • Principe du moindre privilège : n'accordez à l'utilisateur de base de données de l'application que les permissions nécessaires. Une application web ne doit pas avoir les droits DROP TABLE ou GRANT.
  • Validation des entrées : imposez le type et le format attendus (un e-mail ressemble à un e-mail, un nombre à un nombre). Cela seul n'empêche pas l'injection, mais réduit la surface d'attaque.
  • Masquez les messages d'erreur : en production, n'affichez jamais d'erreurs de base brutes à l'utilisateur ; journalisez-les et renvoyez un message générique. Sinon, vous invitez l'injection basée sur les erreurs.
  • WAF et surveillance : un pare-feu applicatif web peut filtrer les schémas d'attaque connus ; insuffisant à lui seul, mais une couche supplémentaire.

Questions fréquentes

Nettoyer l'entrée (sanitize) empêche-t-il l'injection SQL ?

Pas de façon fiable à elle seule. L'échappement des guillemets et les filtres par liste noire peuvent être contournés selon les bases et via des astuces d'encodage. La vraie solution, ce sont les requêtes paramétrées ; le nettoyage n'est qu'une mesure complémentaire.

J'utilise un ORM — y a-t-il encore un risque ?

Pour les requêtes standard, l'ORM vous protège car il lie les valeurs automatiquement. Le risque apparaît là où vous écrivez du SQL brut (whereRaw, DB::raw, concaténation brute). Utilisez toujours des paramètres liés à ces endroits.

Une procédure stockée suffit-elle ?

Pas à elle seule. Si vous construisez du SQL dynamique dans une procédure stockée par concaténation de chaînes, la vulnérabilité se déplace simplement dans la procédure. Vous devez suivre la même approche paramétrée à l'intérieur des procédures.

Ne laissez pas la sécurité de votre application au hasard. Si vous voulez que je passe en revue une base de code existante pour l'injection SQL ou que je construise une couche de données sécurisée, contactez-moi et posons ensemble des fondations solides.

Bu kategorideki tüm yazılar →

Devamı için