L’OWASP (Open Worldwide Application Security Project) est une fondation qui publie des ressources libres sur la sécurité applicative. Son document le plus connu, le Top 10, classe les risques les plus critiques des applications web. L’édition 2025 comprend :
| Rang | Catégorie |
|---|---|
| A01 | Contrôle d’accès défaillant (inclut désormais le SSRF) |
| A02 | Mauvaise configuration de sécurité |
| A03 | Défaillances de la chaîne d’approvisionnement logicielle |
| A04 | Défaillances cryptographiques |
| A05 | Injection |
| A06 | Conception non sécurisée |
| A07 | Défaillances d’authentification |
| A08 | Défaillances d’intégrité des logiciels et des données |
| A09 | Défaillances de journalisation et d’alerte |
| A10 | Mauvaise gestion des conditions exceptionnelles |
Ce n’est pas une norme complète : c’est une liste de sensibilisation. Pour un audit, on s’appuie plutôt sur l’ASVS (Application Security Verification Standard) de l’OWASP. Dans ce cours, nous allons traiter concrètement plusieurs de ces catégories, en commençant par l’injection.
Le mécanisme d’une injection
Une injection se produit quand une donnée fournie par l’utilisateur est interprétée comme du code. Exemple classique en Node.js :
// DANGEREUX : la saisie est collée dans le texte SQL
const email = req.body.email; // "x' OR '1'='1"
const sql = "SELECT * FROM users WHERE email = '" + email + "'";
// => SELECT * FROM users WHERE email = 'x' OR '1'='1'
// La condition est toujours vraie : toute la table est renvoyée.
L’attaquant a fermé la chaîne avec une apostrophe puis ajouté sa propre logique. Selon la base, il peut aussi lire d’autres tables (UNION SELECT) ou modifier des données.
La solution : la requête paramétrée
Avec une requête paramétrée (ou préparée), on envoie au serveur SQL d’un côté la structure, de l’autre les valeurs. La valeur ne peut jamais devenir du SQL, quoi qu’elle contienne.
// Avec le pilote PostgreSQL "pg" : placeholders $1, $2...
const { rows } = await pool.query(
"SELECT id, email FROM users WHERE email = $1",
[email] // valeur transmise séparément
);
// Avec le module natif node:sqlite (Node 22.5+) ou MySQL : placeholders "?"
const stmt = db.prepare("SELECT id, email FROM users WHERE email = ?");
const user = stmt.get(email);
Les ORM (Prisma, Sequelize, Drizzle…) paramètrent automatiquement, sauf si vous utilisez leurs fonctions de requête brute avec concaténation.
Le piège des identifiants
Les placeholders ne fonctionnent que pour des valeurs. Un nom de colonne ou un sens de tri (ORDER BY price DESC) ne peut pas être paramétré. On utilise alors une liste blanche :
const COLONNES_TRI = new Set(["price", "name", "created_at"]);
function clauseTri(col, sens) {
if (!COLONNES_TRI.has(col)) throw new Error("Colonne de tri invalide");
const dir = sens === "desc" ? "DESC" : "ASC"; // deux valeurs possibles seulement
return `ORDER BY ${col} ${dir}`;
}
À retenir
- Ne jamais construire du SQL (ni une commande shell) par concaténation de données externes.
- Remplacer « échapper les apostrophes » par des requêtes paramétrées.
- Valider par liste blanche tout ce qui ne peut pas être paramétré.
- Le même principe vaut pour les autres injections :
child_process.execFileavec un tableau d’arguments plutôt queexecavec une chaîne.
Exercices
Les solutions et les tests restent sur le serveur. Dans l'espace étudiant, ton code est exécuté et corrigé, et le tuteur donne des indices avant la solution.
Construire une requête paramétrée
Écrivez
findUserByEmail(email)qui ne concatène jamais l’email dans le SQL et renvoie un objet{ text, values }(format du pilotepg) :textvaut exactement"SELECT id, email FROM users WHERE email = $1";valuesest un tableau contenant l’email.
Si
emailn’est pas une chaîne, lancez uneTypeError.function findUserByEmail(email) { // À compléter }Liste blanche pour ORDER BY
Écrivez
buildOrderBy(column, direction):- colonnes autorisées :
name,price,created_at; sinon lancez uneError; directionvaut"desc"(insensible à la casse) pourDESC, toute autre valeur donneASC;- renvoie par exemple
"ORDER BY price DESC".
function buildOrderBy(column, direction) { // À compléter }- colonnes autorisées :
Identifier la bonne défense
Quelle pratique protège efficacement une requête SQL contre l’injection ?
- Remplacer les apostrophes par deux apostrophes
- Utiliser une requête paramétrée avec placeholders
- Limiter la longueur du champ à 50 caractères
- Encoder la saisie en Base64 avant de la concaténer
Quiz de fin de leçon
Que signifie l’injection dans l’OWASP Top 10 ?
- Un serveur qui injecte des publicités
- Une donnée externe interprétée comme du code par un interpréteur
- Un paquet npm malveillant
- Un mot de passe trop court
Peut-on passer un nom de colonne en paramètre
$1?- Oui, toujours
- Non : les placeholders ne servent qu'aux valeurs, il faut une liste blanche
- Oui, mais seulement avec MySQL
Quelle fonction Node limite le risque d’injection de commande ?
- child_process.exec avec une chaîne
- child_process.execFile avec un tableau d'arguments
- eval
La suite du cours
- XSS : échapper le HTML et poser une CSP15 min
- Stocker les mots de passe : hachage avec scrypt18 min
- Sessions, jetons signés et cookies sécurisés18 min
- Contrôle d'accès, en-têtes de sécurité et gestion des erreurs18 min