Vulnérabilités webDans le dialogue d’impression, choisissez « Enregistrer au format PDF » comme destination.
Retour

Cybersécurité · C4 Sécurité applicative · Chapitre 1 · 8 h

Vulnérabilités web

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, à part

Dé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.

Quiz · 1 question

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 ?

  • Oui : l'apostrophe échappée neutralise la sortie de chaîne, et les mots-clés dangereux sont bloquéséchappement + liste noire
  • Non : l'échappement manuel et les listes noires se contournent ; seule la requête préparée sépare le code des donnéesséparer structure et données
  • Oui, à condition d'ajouter aussi le mot « DROP » à la liste interditecompléter la liste

Réponse : L'échappement manuel oublie toujours un cas — champs numériques sans apostrophes, encodages alternatifs, autres contextes — et une liste noire de mots-clés se contourne indéfiniment (commentaires intercalés, casse, encodage). Ces approches traitent les symptômes. La requête préparée traite la cause : elle transmet la structure et les données par des canaux distincts, si bien qu'aucune valeur ne peut être interprétée comme du SQL. C'est la seule correction robuste, et elle vaut pour toutes les injections.

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é :

TypeOù est stockée la chargePortée
Stockédans la base (commentaire, profil)frappe tout visiteur de la page — le plus grave
Réfléchidans l'URL, renvoyée telle quellefrappe la victime d'un lien piégé
DOMjamais côté serveur ; le JS de la page l'insèreinvisible 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 :

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 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.

Quiz · 1 question

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 ?

  • Une injection : il faut échapper le paramètre N avant la requêteéchappement d'entrée
  • Un contrôle d'accès défaillant (IDOR) : le serveur authentifie mais ne vérifie pas que le document appartient à l'utilisateurauthentifié ≠ autorisé
  • Un XSS : il faut encoder N avant de l'afficherencodage de sortie

Réponse : La requête est bien formée et N est un entier légitime : il n'y a ni injection ni XSS. La faute est une confusion entre authentification et autorisation — le serveur vérifie QUI vous êtes mais pas ce à quoi vous avez DROIT. C'est l'IDOR, en tête de l'OWASP Top 10. La correction est une vérification d'appartenance côté serveur à chaque requête : le document N est-il bien rattaché au compte appelant ? Masquer les liens ne suffit jamais, puisque l'URL se devine.

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êteRô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: nosniffempêche le navigateur de deviner un type MIME
X-Frame-Options / frame-ancestorsinterdit l'inclusion en cadre (clickjacking)
attributs de cookieHttpOnly, 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 :

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.

Exercice de code

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.

Point de départ

// 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

Solution

// ── ATTAQUE ─────────────────────────────────────────────────────────────────
// Deux entrées qui connectent en admin sans le mot de passe :
connexionVulnerable("admin' --", "peu importe");
//   SQL : ... WHERE login = 'admin' --' AND mdp = '...'
//   Le -- commente la fin : la vérification du mot de passe DISPARAÎT.
connexionVulnerable("x' OR '1'='1", "x' OR '1'='1");
//   La condition devient toujours vraie : le premier utilisateur est renvoyé.
//
// Aucun mot de passe n'a été deviné. On n'a pas cassé l'authentification :
// on a réécrit la REQUÊTE, parce que l'entrée est traitée comme du code.

// ── CORRECTION ──────────────────────────────────────────────────────────────
function connexionSure(login, mdp) {
  const sql = "SELECT * FROM users WHERE login = ? AND mdp = ?";
  const r = requetePreparee(sql, [login, mdp]);
  console.log(r.length ? "  -> connecté : " + r[0].login : "  -> refusé");
  console.log("");
}

connexionSure("amina", "s3cr3t");           // -> connecté
connexionSure("admin' --", "peu importe");  // -> refusé : "admin' --" est
                                            //    cherché LITTÉRALEMENT comme
                                            //    login. Aucun compte ne
                                            //    s'appelle ainsi.
//
// La requête préparée sépare la STRUCTURE (le SQL, avec ses ?) des DONNÉES
// (le tableau de paramètres). Le moteur compile la structure d'abord ; les
// valeurs arrivent ensuite et ne peuvent plus changer le sens de la requête.
// C'est la SEULE correction correcte de l'injection SQL.
//
// Ce qui NE corrige PAS l'injection, et qu'on voit trop souvent :
//   - « échapper les apostrophes » à la main : on en oublie toujours un cas
//     (encodages, guillemets, requêtes numériques sans quotes) ;
//   - une liste noire de mots comme SELECT ou UNION : contournable à l'infini ;
//   - un pare-feu applicatif : utile en défense en profondeur, mais il filtre
//     des motifs, il ne rend pas la requête sûre.
//
// La règle générale, valable pour TOUTE injection (SQL, commandes, LDAP,
// NoSQL) : ne jamais construire une commande par concaténation d'entrée.
// Séparer le code des données, toujours.

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

Flashcards · 4 cartes

Quelle faute unique sous-tend presque toutes les vulnérabilités web, et quelle en est la conséquence pratique ?
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. Conséquence : la correction consiste presque toujours à SÉPARER le code des données (requête préparée contre l'injection SQL, encodage de sortie contre le XSS, arguments séparés contre l'injection de commandes) et à valider toute entrée côté serveur.
Pourquoi la requête préparée corrige-t-elle l'injection SQL là où l'échappement et les listes noires échouent ?
Parce qu'elle transmet la STRUCTURE (le SQL avec ses marqueurs ?) et les DONNÉES (les valeurs) par deux canaux distincts : le moteur compile la structure d'abord, et aucune valeur ne peut ensuite changer le sens de la requête. L'échappement manuel oublie toujours un cas et les listes noires se contournent indéfiniment — ils traitent les symptômes, la requête préparée traite la cause.
Qu'est-ce que l'IDOR, pourquoi est-il en tête de l'OWASP Top 10, et comment le corrige-t-on ?
Une référence directe non contrôlée : on change un identifiant dans l'URL (…/facture?id=1044) et on accède à l'objet d'un autre. Il est en tête parce qu'il incarne la confusion authentification/autorisation, la plus répandue. Correction : vérifier l'autorisation CÔTÉ SERVEUR, à chaque requête, sur chaque objet (cet objet appartient-il à l'appelant ?). Masquer un lien côté client n'est jamais une protection.
Quelles sont les erreurs classiques sur les JWT, et la règle qui les évite ?
Accepter l'algorithme « none » ou laisser le jeton choisir son algorithme de vérification ; ne pas vérifier la signature (le jeton devient réécrivable, role:admin compris) ; oublier qu'un JWT volé reste valide jusqu'à expiration. Règle : un JWT est SIGNÉ, pas chiffré (contenu lisible, donc aucun secret dedans) ; on impose l'algorithme attendu et on vérifie la signature systématiquement, avec une bibliothèque éprouvée.