Chapitre 11 : Bonnes pratiques, injection SQL (pour aller plus loin, cours seul)
Hors sujet : bonnes pratiques et failles SQL
Savoir écrire une requête qui marche ne suffit pas : encore faut-il qu'elle soit lisible, rapide et sûre. Ce chapitre sort un peu du langage SQL lui-même pour parler du métier : les bonnes habitudes à prendre avec n'importe quelle base de données, et les failles les plus courantes, celles qui ont fait fuiter de vrais sites.
Écrire des requêtes propres
Une requête est lue bien plus souvent qu'elle n'est écrite. Quelques habitudes la rendent plus claire, et évitent des bugs qui n'arrivent qu'un mois plus tard.
| À éviter | À préférer | Pourquoi |
|---|---|---|
| SELECT * | SELECT nom, grade | on ne ramène que ce dont on a besoin ; la requête résiste à l'ajout de colonnes |
| FROM a, b WHERE a.id = b.a_id | FROM a JOIN b ON a.id = b.a_id | oublier la condition fait un produit cartésien sans que rien ne le signale |
| des noms comme t1, x, col2 | des alias parlants (e pour enqueteurs) | la requête se lit sans deviner |
| tout sur une seule ligne | un mot-clé principal par ligne | plus facile à relire et à corriger |
-- difficile à relire
select * from affaires a, enqueteurs e where a.enqueteur_id=e.id and a.statut='ouverte';
-- claire
SELECT a.titre, e.nom AS enqueteur
FROM affaires AS a
JOIN enqueteurs AS e ON e.id = a.enqueteur_id
WHERE a.statut = 'ouverte'
ORDER BY a.titre;Modifier des données sans casser
Les erreurs les plus coûteuses viennent presque toujours d'un UPDATE ou d'un DELETE lancé trop vite. Le réflexe : regarder avant d'agir.
-- 1. quelles lignes seront touchées ?
SELECT id, titre, statut FROM affaires WHERE statut = 'en cours';
-- 2. on modifie dans une transaction
BEGIN;
UPDATE affaires SET statut = 'classee' WHERE statut = 'en cours';
-- 3. le nombre de lignes modifiées est-il celui attendu ?
-- oui : COMMIT; non : ROLLBACK;- Toujours un
WHEREsurUPDATEetDELETE(sauf si tu veux vraiment toucher toute la table) ; - Tester sur une copie de la base avant de toucher à la production ;
- Sauvegarder avant une opération lourde, et tester la restauration : une sauvegarde jamais restaurée est une sauvegarde dont on ne sait pas si elle marche ;
- Privilégier la suppression logique quand l'historique compte : une colonne
supprimeoudate_suppressionplutôt qu'unDELETEdéfinitif.
Bien concevoir ses tables
Une base mal conçue coûte cher : chaque requête devient un contournement. Quelques règles de conception évitent l'essentiel des problèmes.
| Règle | Exemple |
|---|---|
| Une clé primaire par table | un `id` entier, jamais un nom ou un e-mail qui peut changer |
| Une information par colonne | pas de colonne `telephones` qui contient `'0601, 0602'` : on crée une table à part |
| Déclarer les clés étrangères et les contraintes | `NOT NULL`, `UNIQUE`, `CHECK` : la base refuse les données absurdes à ta place |
| Stocker la donnée, pas son calcul | la `date_naissance` plutôt que l'`age`, qui devient faux dès demain |
| Choisir le bon type | `DECIMAL` pour l'argent, jamais `REAL` : les nombres à virgule flottante arrondissent |
| Nommer de façon cohérente | toujours le pluriel ou toujours le singulier, toujours `enqueteur_id` pour une clé étrangère |
Garder des requêtes rapides
Sur 100 lignes, tout est rapide. Sur 10 millions, une requête mal écrite fait tomber un site. Un index est comme l'index d'un livre : il permet de retrouver une valeur sans lire toute la table.
-- on cherche souvent les suspects par nom : on l'indexe
CREATE INDEX idx_suspects_nom ON suspects (nom);
-- EXPLAIN montre comment la base compte exécuter la requête
EXPLAIN SELECT * FROM suspects WHERE nom = 'Marechal';- Indexer les colonnes qui servent souvent dans
WHERE,JOINetORDER BY; - Ne pas abuser : chaque index ralentit les
INSERT,UPDATEetDELETE, et prend de la place ; - Ne pas envelopper la colonne dans une fonction :
WHERE UPPER(nom) = 'DUBOIS'ouWHERE YEAR(date) = 2024empêchent souvent d'utiliser l'index ; - Se méfier de
LIKE '%mot': un joker au début oblige à lire toute la table ; - Limiter les résultats (
LIMIT) plutôt que tout ramener pour filtrer ensuite dans le programme.
La faille n°1 : l'injection SQL
L'injection SQL arrive quand un programme fabrique une requête en collant du texte venu de l'utilisateur. Si ce texte contient du SQL, la base l'exécute comme s'il faisait partie de la requête. Cette faille est vieille de plus de vingt ans et figure toujours parmi les plus répandues.
Imagine un formulaire de connexion. Le programme construit la requête en collant les deux champs saisis, entre apostrophes.
-- l'utilisateur tape : vasseur / secret
SELECT * FROM utilisateurs
WHERE nom = 'vasseur' AND mdp = 'secret';Mais si l'utilisateur tape ' OR '1'='1 comme mot de passe, l'apostrophe ferme la chaîne et la suite devient du SQL.
-- l'utilisateur tape : vasseur / ' OR '1'='1
SELECT * FROM utilisateurs
WHERE nom = 'vasseur' AND mdp = '' OR '1'='1';
-- '1'='1' est toujours vrai : la condition passe, la connexion est acceptéePire : en ajoutant des instructions, un attaquant peut lire toutes les tables (avec UNION SELECT), modifier des données, voire supprimer des tables si le compte de l'application en a le droit.
-- l'utilisateur tape comme nom : x'; DROP TABLE utilisateurs; --
SELECT * FROM utilisateurs WHERE nom = 'x'; DROP TABLE utilisateurs; --';Se protéger de l'injection
La parade est simple et fiable : les requêtes préparées (ou paramétrées). On envoie à la base la requête avec des trous, puis les valeurs séparément. Les valeurs ne sont jamais interprétées comme du SQL, quel que soit leur contenu.
SELECT * FROM utilisateurs
WHERE nom = ? AND mdp_hash = ?;
-- les valeurs sont transmises à part par le programme :
-- ici, ' OR '1'='1 est simplement un nom bizarre, sans effet- Toujours utiliser les requêtes préparées de ton langage et de ta bibliothèque, jamais l'assemblage de texte (
+,.format, interpolation de chaîne) ; - Valider les saisies (type, longueur, format), en complément et non à la place ;
- Limiter les droits du compte de l'application (voir le chapitre 9) : même en cas de faille, il ne pourra pas faire de
DROP TABLE; - Ne jamais afficher les messages d'erreur SQL bruts à l'utilisateur : ils révèlent le nom des tables et des colonnes ;
- Utiliser un ORM ou un constructeur de requêtes, qui préparent les requêtes par défaut (mais restent vulnérables si on y colle du texte brut).
Les autres failles classiques
L'injection n'est pas la seule porte d'entrée. Voici les erreurs qui reviennent le plus souvent autour des bases de données.
| Faille | Ce qui se passe | Parade |
|---|---|---|
| Mots de passe en clair | une fuite de la base donne tous les comptes, et les gens réutilisent leurs mots de passe | ne stocker qu'une **empreinte** (bcrypt, argon2) salée, jamais le mot de passe |
| Droits excessifs | le site se connecte avec un compte administrateur ; une seule faille donne tout | un compte par application, avec le **moindre privilège** |
| Messages d'erreur bavards | une erreur SQL affichée à l'écran révèle la structure de la base | message neutre pour l'utilisateur, détail dans les journaux du serveur |
| Base exposée sur Internet | un port de base ouvert à tous, avec un mot de passe par défaut, se fait trouver en quelques minutes | la base n'écoute que le réseau interne, mot de passe fort, pare-feu |
| Sauvegardes mal gardées | un fichier de sauvegarde posé sur un serveur public contient toutes les données | chiffrer les sauvegardes et contrôler qui y accède |
| Données sensibles non chiffrées | carte bancaire, santé, identité : une fuite devient un drame, et une faute au regard du RGPD | ne stocker que le nécessaire, chiffrer le reste |
| Injection au second ordre | une saisie « inoffensive » est stockée telle quelle, puis recollée plus tard dans une autre requête | préparer **toutes** les requêtes, même celles qui relisent la base |
| Requête coûteuse (déni de service) | un filtre sans index ou un `JOIN` géant lancé en boucle met le serveur à genoux | index, `LIMIT`, délai maximal par requête, limitation du nombre d'appels |
Les pièges du langage lui-même
Certaines surprises ne sont pas des failles de sécurité, mais des comportements de SQL qui donnent un résultat faux sans aucune erreur. Ce sont les plus sournois.
NULL n'est égal à rien, pas même à lui-même. Comparer avec = NULL ne renvoie jamais de ligne : il faut écrire IS NULL.
-- ne renvoie JAMAIS rien, sans erreur
SELECT * FROM suspects WHERE alibi = NULL;
-- correct
SELECT * FROM suspects WHERE alibi IS NULL;NOT IN et NULL font mauvais ménage. Si la liste contient un seul NULL, NOT IN ne renvoie plus aucune ligne.
-- si une affaire a un enqueteur_id NULL, le résultat est vide !
SELECT nom FROM enqueteurs
WHERE id NOT IN (SELECT enqueteur_id FROM affaires);
-- plus sûr : NOT EXISTS
SELECT nom FROM enqueteurs e
WHERE NOT EXISTS (SELECT 1 FROM affaires a WHERE a.enqueteur_id = e.id);- Les agrégats ignorent
NULL:AVGne compte pas les lignes vides, la moyenne peut donc surprendre ;COUNT(*)compte les lignes,COUNT(colonne)seulement les valeurs renseignées ; - La division entière : sur certains SGBD,
7 / 2vaut3, pas3.5; on convertit un des deux nombres en décimal ; - Les jointures qui multiplient les lignes : joindre deux tables liées par une relation « plusieurs » fausse un
SUMou unCOUNT;COUNT(DISTINCT ...)ou une sous-requête règle le problème ; - Les comparaisons de texte : la casse et les accents comptent ou non selon le SGBD (
'Dubois'='dubois'?) ; - Le
WHEREfiltre avant le regroupement, leHAVINGaprès : mettre une condition d'agrégat dans leWHEREprovoque une erreur.
En résumé
- Écris des requêtes lisibles : colonnes nommées, jointures explicites, alias parlants ;
- Regarde avant de modifier, travaille en transaction, sauvegarde et teste la restauration ;
- Conçois des tables propres : clés, contraintes, bons types, une information à un seul endroit ;
- Indexe ce qui est souvent cherché, sans abuser ;
- Ne colle jamais de saisie dans une requête : requêtes préparées, toujours ;
- Donne à chaque application le moindre privilège, hache les mots de passe, cache les erreurs ;
- Méfie-toi de
NULL: il est la source de la plupart des résultats faux mais silencieux.