cursus.

Cours 4 · Sécurité applicativeLeçon 1 sur 2

Vulnérabilités web

8 h de lecture11 sections Version PDF

À la fin de cette leçon, vous saurez

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 · vérifiez votre compréhension Sans réponse

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

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 :

  • 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=1043 affiche votre facture ; vous changez 1043 en 1044 et 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.

Quiz · vérifiez votre compréhension Sans réponse

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

  • 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: admin compris ;
  • 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.

Exercice · JavaScript · à vous de jouer

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.

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

Console de sortie
Le résultat s'affiche dans la console

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 · 1 / 4Toucher pour retourner
Fin de la leçon

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.