Skip to Content
SécuritéPartie 6 — Injections6.4 — Défense en profondeur

Chapitre 6.4 — Défense en profondeur

⏱️ TL;DR — Le paramétrage est la parade principale aux injections, mais on ne mise jamais sur une seule couche (rappel Partie 1). Autour, on empile : valider les entrées (type, format, longueur, allow-list) pour réduire la surface ; appliquer le moindre privilège à la base (l’utilisateur SQL de l’appli n’a pas les droits DROP/GRANT, et pas plus de tables que nécessaire) pour borner les dégâts si une injection passe malgré tout ; masquer les erreurs en production (une stacktrace ou une erreur SQL affichée est une carte offerte à l’attaquant) ; et journaliser les anomalies. Ces couches ne remplacent pas le paramétrage — elles le complètent, pour qu’une faille isolée ne devienne pas une catastrophe.

🎯 Objectifs

  • Ajouter la validation d’entrée comme couche complémentaire (pas substitut) au paramétrage.
  • Appliquer le moindre privilège à l’utilisateur de base de données pour borner l’impact.
  • Masquer les erreurs détaillées en production et comprendre pourquoi elles aident l’attaquant.
  • Assembler toutes les couches en une stratégie cohérente contre l’injection.

Couche 1 (principale) : paramétrer

Rappel des chapitres précédents : la défense de référence contre toute injection est de séparer code et données — requêtes préparées / ORM (SQL), arguments en tableau sans shell (command), typage strict (NoSQL), API/échappement dédié (LDAP). C’est la couche qui ferme le mécanisme. Les couches suivantes ne la remplacent pas : elles la renforcent et limitent les dégâts d’un éventuel oubli.

Couche 2 : valider les entrées

La validation d’entrée vérifie qu’une donnée reçue correspond à ce qu’on attend — type, format, longueur, domaine de valeurs — et rejette le reste, au plus tôt. Elle ne remplace pas le paramétrage (une entrée valide peut quand même contenir une apostrophe légitime), mais elle réduit la surface et attrape des anomalies grossières.

// Valider tot, rejeter ce qui ne correspond pas au format attendu const schema = z.object({ email: z.string().email(), age: z.number().int().min(0).max(120), role: z.enum(['apprenant', 'formateur']), // allow-list de valeurs }) const data = schema.parse(req.body) // leve si non conforme

Deux styles à connaître :

  • Allow-list (liste blanche) : n’accepter que ce qui est explicitement autorisé (un rôle parmi une liste, un tri parmi des colonnes connues, un format d’email). Toujours préférable — on définit le permis, on rejette tout le reste.
  • Deny-list (liste noire) : tenter de bloquer les valeurs dangereuses connues (« interdire les apostrophes, DROP, <script>… »). Fragile et à éviter comme défense principale : on oublie toujours un cas, les encodages contournent, et on casse des données légitimes.

⚠️ Piège — Ne fais jamais de la deny-list (filtrer/supprimer les « caractères dangereux ») ta défense contre l’injection. C’est un jeu perdu : les attaquants contournent par encodage (%27, unicode, double-encodage), par variantes de casse, par commentaires SQL, etc. Et « interdire l’apostrophe » casse les vrais noms (O'Connor). La validation allow-list (format attendu) + le paramétrage rendent le filtrage de caractères inutile : une apostrophe dans une valeur paramétrée est inoffensive.

Couche 3 : moindre privilège de la base

Si, malgré tout, une injection SQL passe, jusqu’où peut-elle aller ? Cela dépend des droits de l’utilisateur de base de données que ton appli utilise. Trop souvent, l’appli se connecte avec un compte tout-puissant (propriétaire, voire root SQL). Résultat : une injection peut alors DROP des tables, lire toutes les bases, créer des utilisateurs, lire des fichiers du serveur.

Le principe du moindre privilège (Partie 1) borne les dégâts :

  • L’utilisateur SQL de l’appli n’a que les droits SELECT/INSERT/UPDATE/DELETE sur les tables nécessairespas DROP, ALTER, GRANT, CREATE USER, ni accès aux fonctions système/fichiers.
  • Idéalement, séparer les comptes (un compte en lecture seule pour les parties qui ne font que lire ; un compte avec écriture pour le reste).
  • Pas d’accès aux autres bases/schémas que celui de l’appli.

Ainsi, même une injection réussie voit son impact réduit (lire les tables permises, mais pas détruire la base ni s’échapper vers le système). C’est de la defense in depth appliquée aux droits.

💡 Réflexe — Pose-toi : « si une injection passait maintenant, que pourrait-elle faire avec les droits de mon utilisateur DB ? ». Si la réponse inclut DROP TABLE, lire d’autres bases, ou exécuter des commandes système, c’est que ton compte applicatif est trop privilégié. Réduis-le au strict nécessaire. Le paramétrage empêche l’injection ; le moindre privilège borne la catastrophe si le paramétrage a été oublié quelque part.

Couche 4 : masquer les erreurs en production

Les messages d’erreur détaillés sont une mine d’or pour l’attaquant. Une erreur SQL affichée en clair révèle le type de base, des noms de tables/colonnes, la structure de la requête — exactement ce qu’il faut pour affiner une injection. Une stacktrace expose des chemins, des versions, parfois des secrets.

# ❌ Erreur affichee a l'utilisateur (et a l'attaquant) : SQLSTATE[42S22]: Column not found: 1054 Unknown column 'x' in 'where clause' ... SELECT * FROM users WHERE email = '...' # -> l'attaquant apprend le SGBD, la table, la colonne, la requete

Règle : en production, l’utilisateur ne voit qu’un message générique (« une erreur est survenue »), tandis que le détail part dans les logs internes (pour toi). C’est un réglage de configuration à ne jamais oublier :

  • PHP : display_errors = Off en prod, erreurs vers un log.
  • Symfony : APP_ENV=prod (le profiler et les erreurs détaillées sont désactivés), page d’erreur générique.
  • Next.js/Node : ne pas renvoyer error.stack/error.message bruts au client ; logguer côté serveur, répondre générique.

🎯 Côté attaquant — Une injection aveugle (blind) devient beaucoup plus facile quand l’appli renvoie des erreurs détaillées : chaque message est un retour d’information qui guide l’attaquant vers la structure exacte de la base. À l’inverse, une appli qui répond toujours la même erreur générique force l’attaquant à des techniques plus lentes (booléen, temporel) — et lui laisse moins de prise. Masquer les erreurs n’empêche pas l’injection, mais ralentit et complique massivement l’attaque.

Couche 5 : journaliser les anomalies

Enfin, on journalise les événements suspects (erreurs de requête répétées, entrées rejetées par la validation, payloads d’injection détectés) — sans y mettre de données sensibles (Partie 15). Ces logs alimentent la détection : une salve d’apostrophes et de UNION SELECT sur un endpoint de recherche est un signal d’attaque en cours, à alerter.

La stratégie complète

Aucune couche seule ne suffit ; ensemble, elles font qu’une injection est empêchée (paramétrage), peu probable (validation), bornée si elle passe (moindre privilège), difficile à exploiter à l’aveugle (erreurs masquées) et détectée (logs). C’est la defense in depth appliquée à l’injection.

🧭 Sur FormaCampus — FormaCampus empile les cinq couches : paramétrage partout (Doctrine/$DB), validation des entrées par schémas et allow-lists (jamais de deny-list de « caractères interdits »), un utilisateur PostgreSQL applicatif limité au SELECT/INSERT/UPDATE/DELETE sur son schéma (pas de DROP/GRANT, pas d’accès aux autres schémas), les erreurs masquées en prod (APP_ENV=prod, page générique, détail en logs), et une journalisation des payloads suspects qui alerte l’équipe. Une injection oubliée quelque part ne pourrait ni détruire la base, ni sortir du schéma, ni s’exploiter facilement à l’aveugle — et serait repérée.

✏️ Exercices

Exercice 1 — Allow-list vs deny-list. Un dev « sécurise » une recherche en supprimant les apostrophes et le mot DROP de l’entrée avant de la concaténer dans la requête. Explique pourquoi c’est insuffisant et ce qu’il faut faire.

✅ Solution

C’est une deny-list, fragile et contournable : l’attaquant encode l’apostrophe (%27, unicode), casse le mot (DR/**/OP, casse mixte), ou utilise d’autres vecteurs (OR 1=1, commentaires, sous-requêtes) que le filtre ne couvre pas — et supprimer l’apostrophe corrompt les données légitimes (O'Connor). La bonne approche n’est pas de filtrer des caractères mais de paramétrer la requête (couche 1), éventuellement doublée d’une validation allow-list du format attendu (couche 2). Une fois paramétrée, l’apostrophe est inoffensive — il n’y a plus rien à filtrer.

Exercice 2 — Borne l’impact. Ton appli se connecte à MySQL avec un compte ayant tous les droits sur toutes les bases. Une SQLi est découverte sur un endpoint. Quelles mesures de moindre privilège auraient limité les dégâts ?

✅ Solution

Créer un utilisateur applicatif dédié avec seulement SELECT/INSERT/UPDATE/DELETE sur les tables de l’appli, sans DROP/ALTER/CREATE/GRANT, sans accès aux autres bases, et sans privilèges système (lecture de fichiers, FILE). Idéalement, séparer un compte lecture seule (pour les endpoints qui ne font que lire) d’un compte en écriture. Ainsi, l’injection découverte n’aurait pu, au pire, que lire/modifier les tables permises — pas détruire la base, lire d’autres bases, ni pivoter vers le système. Le moindre privilège transforme une catastrophe potentielle en incident borné (et le paramétrage aurait empêché l’injection en premier lieu).

🧠 Quiz de révision

1. La validation d’entrée remplace-t-elle le paramétrage ?

Non : elle le complète. La validation (type, format, allow-list) réduit la surface et attrape des anomalies, mais une entrée valide peut contenir une apostrophe légitime. Le paramétrage reste la couche qui ferme le mécanisme d’injection. On fait les deux.

2. Pourquoi préférer l’allow-list à la deny-list ?

L’allow-list définit ce qui est autorisé et rejette tout le reste (robuste). La deny-list tente de bloquer le « dangereux connu » : on oublie toujours des cas, les encodages contournent, et on casse des données légitimes. La deny-list ne doit jamais être la défense principale.

3. Comment le moindre privilège de la base limite-t-il une injection ?

En bornant les droits du compte SQL applicatif (pas de DROP/GRANT, tables et bases limitées, pas d’accès système) : même si une injection passe, elle ne peut faire que ce que le compte permet — pas détruire la base ni pivoter. C’est la defense in depth appliquée aux droits.

4. Pourquoi masquer les erreurs détaillées en production ?

Parce qu’une erreur SQL/stacktrace affichée révèle le SGBD, des noms de tables/colonnes, la structure des requêtes, des chemins et versions — autant d’aide pour affiner une injection (surtout en aveugle). En prod : message générique à l’utilisateur, détail dans les logs internes.

5. Résume la stratégie de defense in depth contre l’injection.

Paramétrer (empêche), valider en allow-list (réduit la surface), moindre privilège de la base (borne les dégâts), masquer les erreurs en prod (complique l’exploitation aveugle) et journaliser les anomalies (détecte). Aucune couche seule ne suffit ; ensemble, elles rendent l’injection improbable et sans gravité.


Fin de la Partie 6. Les injections côté serveur sont fermées : un principe (séparer code et données), une parade (paramétrer), des couches autour. On passe maintenant au versant navigateur de l’injection avec la Partie 7 — XSS & sécurité côté client : quand une entrée devient du JavaScript exécuté chez la victime.

Last updated on