Le bloc le plus employable : OWASP Top 10, injection SQL, XSS, CSRF, contrôle d'accès défaillant, désérialisation, SSRF, en-têtes et CSP, sécurité des API et des JWT.
C'est le chapitre le plus long du cours, et le plus employable. La quasi-totalité des vulnérabilités qu'un diplômé de licence rencontrera dans un premier emploi sont ici, parce que c'est ici qu'est la surface d'attaque : une application web est exposée à Internet en permanence, elle accepte des entrées de n'importe qui, et le chapitre 6 a montré que le pare-feu ne comprend pas ce qui circule dans le flux HTTP qu'il autorise.
Une idée gouverne tout ce qui suit, et vaut la peine d'être posée avant les techniques :
Presque toutes les vulnérabilités web sont une variante d'une même faute — mélanger les données de l'utilisateur avec le code ou la logique de l'application, et faire confiance à ce qui vient du client.
Vous l'avez déjà croisée : le SUID qui fait confiance au PATH (chapitre 4), l'ARP qui croit la
dernière réponse (chapitre 5). Sur le web, elle prend une dizaine de formes, que l'OWASP Top
10 classe et hiérarchise.
L'OWASP Top 10
L'OWASP Top 10 est le référentiel de fait des risques applicatifs web, révisé tous les trois ou quatre ans à partir de données réelles. Il ne faut pas l'apprendre par cœur, mais en comprendre la structure : il classe par risque, pas par difficulté technique, exactement au sens du chapitre 1.
Le fait le plus instructif est ce qui figure en tête : depuis plusieurs éditions, la première place n'est pas une faille exotique mais le contrôle d'accès défaillant — la confusion entre authentification et autorisation du chapitre 3. La deuxième grande famille reste les injections, dont l'injection SQL est l'archétype. Ce chapitre traite les plus structurantes ; le principe compte plus que l'exhaustivité.
L'injection SQL
C'est la vulnérabilité fondatrice, et le TP central de ce cours. Elle naît quand une application construit une requête SQL en collant l'entrée de l'utilisateur dans la chaîne :
SELECT * FROM users WHERE login = '<saisie>' AND mdp = '<saisie>'Si l'attaquant saisit comme login admin' --, la requête devient :
SELECT * FROM users WHERE login = 'admin' --' AND mdp = '...'Le -- commente la fin : la vérification du mot de passe disparaît, et l'attaquant se
connecte en admin sans le connaître. La cause profonde est que le moteur ne fait aucune
différence entre le code que le développeur a écrit et les données que l'utilisateur a
fournies. Tout le reste — extraction de la base entière par UNION, injection à l'aveugle
déduite des temps de réponse, exécution de commandes système — n'est qu'une élaboration de cette
faute unique.
La correction n'est ni l'échappement, ni une liste de mots interdits. C'est la requête
préparée (requête paramétrée) : le SQL est envoyé avec des marqueurs ?, et les valeurs sont
transmises séparément. Le moteur compile la structure d'abord ; les données arrivent ensuite
et ne peuvent plus en changer le sens.
SELECT * FROM users WHERE login = ? AND mdp = ? -- structure["admin' --", "peu importe"] -- données, à partDésormais admin' -- est cherché littéralement comme login : aucun compte ne porte ce nom,
l'attaque échoue. C'est la seule correction correcte, et elle se généralise à toute injection
— commandes système, LDAP, NoSQL : ne jamais construire une commande par concaténation ; séparer
le code des données, toujours.
L'exercice de ce chapitre est exactement ce fil : vous montez l'attaque, puis vous écrivez le correctif. Faites-le maintenant si vous préférez, il éclaire tout le reste.
Pour corriger une injection SQL, un développeur remplace les apostrophes de l'entrée par des doubles apostrophes et interdit les mots « UNION » et « SELECT ». Est-ce suffisant ?
Le XSS : injection, côté navigateur
Le cross-site scripting est la même faute, déplacée : au lieu d'injecter du SQL dans la base, on injecte du JavaScript dans la page d'une autre victime. Le code s'exécute alors dans son navigateur, avec ses droits — et peut voler son cookie de session (chapitre 3), agir en son nom, défigurer la page, ou l'hameçonner.
Trois formes, selon le trajet du code injecté :
| Type | Où est stockée la charge | Portée |
|---|---|---|
| Stocké | dans la base (commentaire, profil) | frappe tout visiteur de la page — le plus grave |
| Réfléchi | dans l'URL, renvoyée telle quelle | frappe la victime d'un lien piégé |
| DOM | jamais côté serveur ; le JS de la page l'insère | invisible aux défenses serveur |
La correction a, ici aussi, un principe unique : l'encodage de sortie contextuel. Toute
donnée insérée dans une page est encodée selon le contexte où elle apparaît — corps HTML,
attribut, URL, bloc JavaScript n'ont pas les mêmes règles. En pratique, on s'appuie sur un moteur
de gabarits qui échappe par défaut (et l'on se méfie des échappatoires du type
dangerouslySetInnerHTML ou innerHTML). Deux renforts : le cookie de session en HttpOnly,
qui le rend inaccessible au JavaScript volé, et la politique de sécurité de contenu (CSP),
traitée plus bas.
CSRF : abuser d'une session valide
La falsification de requête intersite exploite le fait que le navigateur joint automatiquement les cookies à chaque requête vers un domaine. Un site malveillant fait émettre, depuis le navigateur d'une victime déjà connectée à sa banque, une requête de virement — et la banque la traite, puisque le cookie de session valide l'accompagne.
Noter la différence avec le XSS, qu'on confond souvent : le XSS exécute du code sur le site vulnérable ; le CSRF ne fait qu'émettre une requête en abusant d'une session existante, sans lire la réponse. Les défenses :
- un jeton anti-CSRF imprévisible, exigé pour toute action modifiante et absent d'un site tiers ;
- l'attribut de cookie
SameSite, qui empêche l'envoi automatique du cookie depuis un autre site — devenu la première ligne de défense ; - pour les API, ne pas dépendre d'une authentification par cookie ambiant.
Le contrôle d'accès défaillant
Première place de l'OWASP Top 10, et matérialisation exacte de la distinction du chapitre 3 : authentifié n'est pas autorisé. Deux formes reviennent constamment :
- La référence directe non contrôlée (IDOR). L'URL
…/facture?id=1043affiche votre facture ; vous changez1043en1044et vous voyez celle d'un autre client. Le serveur a vérifié que vous étiez connecté, jamais que cette facture était la vôtre. - L'élévation verticale. Un utilisateur ordinaire appelle directement
…/admin/users: si le serveur se contente de cacher le lien dans l'interface sans vérifier le rôle côté serveur, l'action réussit.
La correction tient en une règle : vérifier l'autorisation côté serveur, à chaque requête, sur chaque objet, sur le principe du refus par défaut. Ce qui est fait côté client — masquer un bouton, désactiver un champ — est du confort, jamais de la sécurité : le client est entre les mains de l'attaquant.
Une application affiche les documents à l'URL /document?id=N. Le serveur vérifie que l'utilisateur est connecté, puis renvoie le document N. Un utilisateur incrémente N et lit les documents d'autres comptes. Quelle est la nature de la faille et sa correction ?
Désérialisation, SSRF, et les autres
Trois familles à connaître, chacune une variante du même excès de confiance envers l'entrée.
La désérialisation non sécurisée reconstruit un objet à partir de données reçues. Si le format permet d'instancier des types arbitraires ou de déclencher du code à la reconstruction, un flux forgé mène à l'exécution de code à distance. La règle : ne jamais désérialiser une donnée non fiable dans un format qui autorise le code ; préférer des formats de données purs (JSON avec un schéma strict) et vérifier l'intégrité.
Le SSRF (server-side request forgery) trompe le serveur pour qu'il émette lui-même une requête vers une cible que l'attaquant choisit — typiquement un service interne inaccessible de l'extérieur, ou le service de métadonnées d'un hébergeur cloud, qui délivre des identifiants. Le serveur, digne de confiance sur le réseau interne (chapitre 6), devient le relais de l'attaquant. La défense : liste blanche stricte des destinations autorisées, jamais une liste noire, et segmentation.
L'injection de commandes système est l'injection SQL transposée au shell : une entrée collée dans une commande passée à l'interpréteur. Même cause, même correction — appeler le programme avec ses arguments séparés, jamais via une chaîne de shell concaténée.
En-têtes de sécurité et CSP
Le navigateur applique des défenses que le serveur active par des en-têtes HTTP. Elles ne corrigent pas les failles à la source, mais forment une couche de défense en profondeur, à coût quasi nul.
| En-tête | Rôle |
|---|---|
Strict-Transport-Security (HSTS) | force HTTPS, neutralise le déclassement (chapitre 5) |
Content-Security-Policy (CSP) | restreint d'où le code et les ressources peuvent être chargés |
X-Content-Type-Options: nosniff | empêche le navigateur de deviner un type MIME |
X-Frame-Options / frame-ancestors | interdit l'inclusion en cadre (clickjacking) |
| attributs de cookie | HttpOnly, Secure, SameSite |
La CSP mérite une mention particulière : bien réglée, elle atténue le XSS en interdisant l'exécution de scripts en ligne et en limitant les sources de code autorisées. Un script injecté malgré tout ne s'exécute pas s'il ne vient pas d'une source déclarée. Ce n'est pas un substitut à l'encodage de sortie — c'est un filet de sécurité pour le jour où l'encodage a été oublié quelque part. C'est l'esprit même de la défense en profondeur : supposer que la première barrière cédera.
Sécurité des API et des JWT
Les applications modernes exposent des API, qui héritent de tout ce qui précède — injection, contrôle d'accès, SSRF — et ajoutent leurs propres pièges : contrôle d'accès au niveau de chaque objet et de chaque fonction (l'IDOR est la faille reine des API), absence de limitation de débit, exposition excessive de données « que le client filtrera » — alors que le client est justement l'attaquant.
Les JWT (chapitre 3) concentrent les erreurs, et elles sont spécifiques :
- accepter l'algorithme
none, ou laisser le jeton dicter son propre algorithme de vérification — le serveur doit imposer l'algorithme attendu ; - ne pas vérifier la signature du tout — le jeton n'est alors qu'un cookie que l'attaquant
réécrit à sa guise,
role: admincompris ; - oublier la révocation : un JWT volé reste valide jusqu'à expiration (la limite déjà vue au chapitre 3).
La règle générale : un JWT est un jeton signé, pas chiffré — son contenu est lisible par quiconque. On n'y met donc jamais de secret, et l'on vérifie la signature et l'algorithme, systématiquement, avec une bibliothèque éprouvée (la règle du chapitre 2 : ne jamais implémenter la crypto soi-même).
À vous
Le TP central du cours. Une page de connexion construit sa requête SQL par concaténation.
D'abord, attaquez : connectez-vous en administrateur sans connaître son mot de passe — par la tautologie, puis par le commentaire. Ensuite, corrigez : réécrivez la fonction avec une requête préparée, et vérifiez que la même attaque échoue désormais. Notez au passage ce qui ne corrige pas l'injection — c'est aussi instructif que ce qui la corrige.
Une page de connexion construit sa requête SQL par concaténation. Connectez-vous en admin sans son mot de passe (deux méthodes), puis réécrivez la fonction avec une requête préparée pour que l'attaque échoue. Notez au passage ce qui NE corrige pas l'injection.
// Un formulaire de connexion. La requête est construite en COLLANT // l'entrée de l'utilisateur dans la chaîne SQL — l'erreur d'origine de // l'injection, et elle reste la faille n°1 du web. const UTILISATEURS = [ { id: 1, login: "amina", mdp: "s3cr3t", role: "user" }, { id: 2, login: "admin", mdp: "xK9#mP2!", role: "admin" }, ]; // Un mini-moteur SQL : il ne comprend que ce dont on a besoin ici. // Ce qui compte : il ne fait AUCUNE différence entre le code que le // développeur a écrit et les données que l'utilisateur a fournies. C'est // TOUTE la vulnérabilité. function executer(sql) { console.log("SQL exécuté : " + sql); const m = sql.match(/WHERE login = '(.*)' AND mdp = '(.*)'/); if (!m) return []; let [, condLogin, condMdp] = [m[0], m[1], m[2]]; return UTILISATEURS.filter(u => { const okLogin = estVrai(condLogin, u.login); const okMdp = estVrai(condMdp, u.mdp, true); return okLogin && okMdp; }); } // estVrai gère le littéral normal ET la tautologie ' OR '1'='1 injectée. function estVrai(cond, valeur, finDeChaine) { if (cond.includes("OR '1'='1")) return true; // injection réussie if (finDeChaine && cond.includes("--")) return true; // commentaire : mdp ignoré return cond === valeur; } // ── La fonction vulnérable ────────────────────────────────────────────────── function connexionVulnerable(login, mdp) { const sql = "SELECT * FROM users WHERE login = '" + login + "' AND mdp = '" + mdp + "'"; const r = executer(sql); console.log(r.length ? " -> connecté en tant que " + r[0].login + " (" + r[0].role + ")" : " -> refusé"); console.log(""); } // ── ATTAQUE ───────────────────────────────────────────────────────────────── console.log("=== Attaque ==="); connexionVulnerable("amina", "s3cr3t"); // cas normal connexionVulnerable("admin", "mauvais"); // refusé, normal // À VOUS : trouvez une entrée de login (ou de mdp) qui connecte en ADMIN // SANS connaître son mot de passe. Deux classiques : la tautologie, et le // commentaire qui fait disparaître la vérification du mot de passe. connexionVulnerable("???", "???"); // ── CORRECTION — à vous ───────────────────────────────────────────────────── // Réécrivez la connexion avec une REQUÊTE PRÉPARÉE simulée : le SQL contient // des paramètres (?), et les valeurs sont passées À PART, jamais collées. console.log("=== Correctif ==="); function requetePreparee(sql, params) { // Le moteur reçoit la structure ET les données séparément : une valeur // n'est JAMAIS interprétée comme du SQL. const trouve = UTILISATEURS.filter(u => u.login === params[0] && u.mdp === params[1]); console.log("préparé : " + sql + " params=" + JSON.stringify(params)); return trouve; } function connexionSure(login, mdp) { // à compléter en utilisant requetePreparee(...) } connexionSure("amina", "s3cr3t"); connexionSure("admin' --", "peu importe"); // l'attaque doit désormais ÉCHOUER
Ce que la suite en fait
Ce chapitre a traité les vulnérabilités des applications qui s'exécutent dans un environnement géré — un interpréteur, un moteur de base, un navigateur. Le chapitre 8 descend d'un cran, vers le logiciel natif, où l'application gère elle-même sa mémoire : là, la même faute — faire confiance à une entrée non validée — ne renvoie plus une facture qui n'est pas la vôtre, elle écrase la pile et détourne l'exécution.
Vous retrouverez le principe directeur intact — séparer le code des données, valider toute entrée — et la même logique de défense en couches, l'encodage et la CSP d'ici trouvant leur écho dans l'ASLR et le DEP de là.
À retenir
Vous avez parcouru les 11 sections.
Marquez-la terminée pour faire avancer votre parcours, ou revenez sur un point avant de passer à la suite.