Objectifs
À la fin de ce cours, tu seras capable de :
- Identifier les points faibles d'un parcours d'authentification complet : inscription, connexion, réinitialisation de mot de passe, 2FA, gestion de session et jetons JWT.
- Analyser un flux de réinitialisation pour repérer un lien construit à partir de l'en-tête
Hostou un jeton prédictible, réutilisable, non expirant ou divulgué. - Démontrer une faiblesse d'authentification par la prise de contrôle de ton propre second compte de test, sans jamais toucher un compte réel.
- Évaluer la sévérité CVSS 3.1 d'un défaut d'authentification en tenant compte de l'interaction utilisateur et des prérequis réels.
- Rédiger une recommandation de correction précise : jetons aléatoires à usage unique et de courte durée, URL de base fixe, algorithme imposé à la vérification, rotation de session, limitation de débit.
Pourquoi c'est important en bug bounty
L'authentification est la porte d'entrée de toute l'application : si elle cède, toutes les autres protections deviennent inutiles. L'OWASP Top 10 2021 lui consacre la catégorie A07 — Identification and Authentication Failures, et l'OWASP API Security Top 10 2023 la classe en API2 — Broken Authentication. Dans les programmes publics, les rapports de prise de contrôle de compte (« Account Takeover », ATO) figurent parmi les mieux rémunérés : une ATO sans interaction est généralement classée Critique, une ATO qui nécessite un clic de la victime Élevée.
Ce qui distingue un rapport payé d'un rapport fermé tient presque toujours à trois éléments :
- Un impact concret et démontré. « Le jeton de reset fait 6 chiffres » est une observation ; « le jeton de reset est renvoyé dans la réponse JSON, ce qui permet de réinitialiser le mot de passe de n'importe quel compte dont on connaît l'e-mail » est une vulnérabilité.
- Le respect des exclusions. Beaucoup de programmes excluent explicitement l'énumération d'utilisateurs, l'absence de limitation de débit sans impact démontré, la déconnexion par CSRF ou les politiques de mot de passe faibles. Lis la politique avant de rédiger.
- Une preuve réalisée uniquement sur tes comptes. Un hunter qui démontre une ATO sur le compte d'un vrai utilisateur sort du cadre autorisé, même si la faille est réelle.
Le mécanisme
Toutes les failles d'authentification partagent une même cause racine : le serveur accorde sa confiance à une donnée que le client contrôle, peut deviner ou peut rejouer. Selon la fonctionnalité, cette donnée change de forme.
Réinitialisation de mot de passe
Le serveur génère un jeton, l'envoie par e-mail dans un lien, puis accepte ce jeton comme preuve que l'utilisateur possède la boîte mail. Quatre défauts reviennent constamment :
- Lien construit depuis l'en-tête
Host(ouX-Forwarded-Host,X-Host,Forwarded) : le framework fabrique l'URL absolue à partir de la requête entrante. Si l'attaquant déclenche le reset de la victime avec unHostqu'il contrôle, l'e-mail authentique contient un lien vers son domaine. Le clic de la victime — ou le préchargement automatique du lien par un filtre de messagerie — lui transmet le jeton. - Jeton prédictible : horodatage, identifiant incrémental, hachage MD5 de l'e-mail, ou générateur pseudo-aléatoire non cryptographique (
Math.random(),rand(),random.random()). - Jeton non expirant ou réutilisable : un lien reçu il y a six mois fonctionne encore, ou le même jeton permet plusieurs changements de mot de passe.
- Jeton renvoyé dans la réponse : l'API répond au
POST /resetavec un objet contenant le jeton, souvent un reste de débogage. Plus besoin d'accéder à la boîte mail.
Double authentification (2FA)
La 2FA ajoute une seconde étape, mais elle n'est efficace que si le serveur lie l'état de l'étape 2 à la session ouverte par l'étape 1 et refuse tout accès tant que l'étape 2 n'est pas validée. Les défauts classiques :
- Étape contournable : après le mot de passe, le serveur crée déjà une session pleinement authentifiée ; la page de saisie du code n'est qu'une redirection côté client. Naviguer directement vers
/accountsuffit. - Code non lié à la session ou au compte : le code valide pour le compte A est accepté pour le compte B, ou l'identifiant de l'utilisateur à valider est un paramètre modifiable (
user_iddans le corps de la requête de vérification). - Absence de limitation : un code à 6 chiffres offre un million de combinaisons ; sans plafond de tentatives ni invalidation après quelques échecs, il ne protège presque rien.
Sessions
Le cookie de session est l'équivalent d'un mot de passe temporaire. Deux défauts majeurs :
- Fixation de session : l'identifiant de session attribué avant la connexion est conservé après. Si un attaquant peut imposer cet identifiant à la victime (paramètre d'URL, sous-domaine qui pose un cookie), il partage sa session une fois qu'elle s'est authentifiée.
- Non-invalidation : la déconnexion, le changement de mot de passe ou la désactivation de la 2FA laissent les autres sessions — et les anciens jetons — actives. Une session volée reste exploitable même après que la victime a « tout sécurisé ».
JWT
Un JSON Web Token (RFC 7519) se compose de trois segments encodés en Base64URL séparés par des points : un en-tête (algorithme alg, type, éventuellement kid), une charge utile (les « claims » : sub, role, exp, aud…) et une signature. Le point essentiel : l'en-tête et la charge utile sont lisibles par tous — encodés, pas chiffrés. Toute la sécurité repose sur la vérification de la signature côté serveur.
Les défauts proviennent presque toujours d'une bibliothèque mal configurée qui fait confiance au champ alg fourni par le jeton lui-même :
alg: noneaccepté : la RFC 7518 définit un algorithme « none » pour des jetons non signés ; une bibliothèque qui l'accepte en vérification valide n'importe quelle charge utile.- Confusion d'algorithme : le serveur signe en RS256 (clé privée / clé publique). Si la vérification lit
algdans le jeton et accepte HS256, elle utilise la clé publique — connue de tous — comme secret HMAC. - Secret HMAC faible : un secret court ou issu d'un tutoriel (
secret,changeme) se teste hors ligne contre une liste de secrets connus. expnon vérifiée : un jeton expiré depuis des mois reste accepté, ce qui transforme toute fuite de jeton (logs, historique, Referer) en accès permanent.
Question de réflexion : pourquoi un jeton de reset de 128 bits parfaitement aléatoire ne protège-t-il pas contre l'empoisonnement de l'en-tête Host ?
Parce que l'attaque ne cherche pas à deviner le jeton : elle s'arrange pour que le serveur l'envoie lui-même à l'attaquant. Le jeton est généré correctement et livré dans la vraie boîte mail de la victime, mais le lien pointe vers un domaine contrôlé par l'attaquant. L'entropie du jeton est sans effet ; seule la source de l'URL de base compte.
Où chercher
La surface d'authentification est plus large qu'un simple formulaire de connexion :
| Fonctionnalité | Signaux faibles à observer |
|---|---|
| Mot de passe oublié | Lien dont le domaine change selon Host, jeton court ou numérique, jeton présent dans la réponse HTTP, même jeton à chaque demande |
| Inscription / vérification d'e-mail | Compte utilisable avant vérification, lien de vérification réutilisable |
| Connexion | Messages d'erreur différents selon l'existence du compte, absence de verrouillage progressif |
| 2FA | Cookie de session complet délivré avant le code, paramètre user_id dans la vérification, aucune limite de tentatives |
| Changement d'e-mail / mot de passe | Pas de ressaisie du mot de passe actuel, anciennes sessions conservées |
| API mobiles et anciennes versions | /api/v1/ moins protégée que /api/v2/, absence de 2FA sur l'API |
| Jetons JWT | En-tête Authorization: Bearer eyJ..., cookie commençant par eyJ, champ alg, kid ou jku dans l'en-tête, absence de exp |
Garde en tête que les équipes testent souvent le parcours web principal, mais oublient les chemins secondaires : application mobile, API partenaire, ancienne version d'un endpoint, flux « magic link ».
Méthode de test
Prépare deux comptes de test qui t'appartiennent (A : « attaquant », B : « victime »), chacun avec une adresse e-mail que tu contrôles, par exemple via des alias. Fais transiter tout le trafic par ton proxy d'interception et conserve l'historique : la comparaison des réponses est ton principal outil.
Étape 1 — Cartographier le flux. Effectue une réinitialisation complète et légitime sur le compte A. Note chaque requête, le format du jeton, sa longueur, son emplacement (URL, corps, cookie) et le domaine du lien reçu.
Étape 2 — Tester l'origine du lien. Déclenche un reset pour le compte B en modifiant l'en-tête Host, puis en ajoutant un en-tête de réécriture :
POST /api/password-reset HTTP/1.1
Host: hunter-collab.exemple.fr
Content-Type: application/json
{"email": "compte-b@exemple.fr"}Ouvre ensuite la boîte mail du compte B (la tienne) : si le lien reçu pointe vers le domaine fourni dans la requête, la vulnérabilité est confirmée. L'e-mail suffit comme preuve. Si Host est filtré par le reverse proxy, répète le test avec les en-têtes de réécriture courants (X-Forwarded-Host, Forwarded), un à la fois.
Étape 3 — Examiner le jeton. Demande trois ou quatre resets successifs pour le compte A et compare les jetons : longueur, alphabet, parties communes, corrélation avec l'heure. Vérifie dans le proxy si la réponse au POST contient le jeton ou le lien. Teste le cycle de vie : le jeton fonctionne-t-il deux fois ? Après 24 heures ? Après la demande d'un nouveau jeton ?
Étape 4 — Tester la 2FA. Active la 2FA sur le compte A. Connecte-toi avec le mot de passe, puis, au lieu de saisir le code, observe le cookie délivré à ce stade et essaie d'ouvrir directement une page authentifiée. Si elle s'affiche, l'étape 2 n'est qu'une formalité côté client. Vérifie ensuite si la requête de validation du code contient un identifiant d'utilisateur modifiable, et si un nombre limité de codes erronés entraîne un blocage. Ne lance pas de série massive de tentatives : quelques essais suffisent à observer l'absence de limitation, que tu décris ensuite.
Étape 5 — Tester le cycle de vie des sessions. Ouvre deux sessions sur le compte A (deux navigateurs). Change le mot de passe dans l'une : l'autre reste-t-elle valide ? Déconnecte-toi : l'ancien cookie, rejoué depuis le proxy, est-il encore accepté ? Compare aussi l'identifiant de session avant et après la connexion : s'il ne change pas, note un risque de fixation.
Étape 6 — Lire les JWT. Décode l'en-tête et la charge utile de ton propre jeton (un simple décodage Base64URL suffit). Relève l'algorithme, la présence et la valeur de exp, les claims de rôle ou d'identifiant. Ces observations orientent le diagnostic : un jeton sans exp, un alg symétrique avec un secret possiblement faible, ou un claim de rôle utilisé côté serveur méritent d'être signalés. Toute vérification active se fait avec tes comptes et s'arrête dès que le comportement est établi.
Variantes et défenses courantes
- Limitation de débit par IP seulement : elle n'empêche pas une attaque distribuée ni le test lent d'un code 2FA ; la limite doit porter sur le compte et le jeton.
- Jeton de reset haché mais non lié à l'utilisateur : un jeton valide pour A qui fonctionne en changeant l'e-mail dans la requête finale vers B.
- Validation de
Hostpar liste noire : les en-têtes de réécriture (X-Forwarded-Host) contournent souvent le contrôle s'ils sont lus par le framework. - Magic links (connexion par lien e-mail) : mêmes risques que le reset, avec un impact direct puisque le lien ouvre une session.
- 2FA appliquée au web mais pas à l'API mobile ou à une ancienne version d'API.
- JWT avec
kidoujku: si le serveur choisit sa clé de vérification à partir de ces champs sans liste blanche, la confiance est déplacée vers le client. - Changement d'e-mail sans confirmation suivi d'un reset : une CSRF ou une XSS sur le changement d'e-mail devient une prise de contrôle complète.
Question de réflexion : l'énumération d'utilisateurs est-elle une vulnérabilité à signaler ?
Seule, rarement : la plupart des programmes l'excluent explicitement, car l'information (« ce compte existe ») a peu de valeur et la corriger complètement nuit à l'ergonomie. Elle devient pertinente si elle expose des données sensibles (un service de santé révélant qu'une personne y est inscrite) ou si elle est un maillon nécessaire d'une chaîne démontrée, par exemple combinée à une absence de limitation qui rend une attaque de mot de passe réaliste. Lis toujours la politique avant de la rapporter.
Prouver l'impact sans nuire
La règle d'or : la victime est toujours ton second compte de test.
- Empoisonnement du
Host: capture de l'e-mail reçu sur le compte B, montrant le lien vers le domaine fourni. Il n'est pas nécessaire de cliquer depuis un vrai utilisateur ni de réutiliser le jeton. - Jeton dans la réponse ou prédictible : montre que tu as réinitialisé le mot de passe du compte B depuis la session du compte A, puis rétablis l'accès.
- 2FA contournable : montre l'accès à une page authentifiée du compte A sans avoir saisi le code.
- Sessions non invalidées : montre qu'un cookie du compte A reste accepté après changement de mot de passe.
- JWT : décris le défaut de vérification constaté sur ton propre jeton. N'utilise jamais un jeton modifié pour accéder au compte de quelqu'un d'autre.
Interdits : tenter une prise de contrôle sur un compte que tu ne possèdes pas, lancer des attaques de mots de passe à grande échelle, verrouiller des comptes réels.
Évaluer la sévérité
| Scénario | Vecteur CVSS 3.1 | Score |
|---|---|---|
| Jeton de reset renvoyé dans la réponse : prise de contrôle de tout compte dont on connaît l'e-mail | AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N | 9.1 |
Empoisonnement du Host : la victime doit cliquer sur le lien reçu | AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N | 8.1 |
JWT alg: none accepté : usurpation de n'importe quel utilisateur, y compris administrateur | AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H | 9.8 |
| Énumération d'utilisateurs exposant l'inscription à un service sensible | AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N | 5.3 |
Pour un contournement de 2FA, n'oublie pas que l'attaquant doit déjà connaître le mot de passe : la complexité d'attaque ou les prérequis doivent le refléter, et beaucoup de programmes appliquent leur propre grille pour ce cas.
Corriger
Génération de lien vulnérable : l'URL de base vient de la requête et le jeton n'est ni aléatoire cryptographiquement ni limité dans le temps.
// VULNÉRABLE
app.post("/api/password-reset", async (req, res) => {
const user = await db.users.findByEmail(req.body.email);
const token = Math.random().toString(36).slice(2);
await db.resets.insert({ userId: user.id, token });
const link = `${req.protocol}://${req.get("host")}/reset?token=${token}`;
await mail.send(user.email, link);
res.json({ ok: true, token });
});Version corrigée : URL de base fixe, jeton aléatoire stocké haché, durée de vie courte, usage unique, réponse identique que le compte existe ou non.
// CORRIGÉ
import { randomBytes, createHash } from "node:crypto";
const APP_URL = process.env.APP_URL; // ex. https://app.acme.test
app.post("/api/password-reset", rateLimit({ key: (r) => r.body.email }), async (req, res) => {
const user = await db.users.findByEmail(req.body.email);
if (user) {
const token = randomBytes(32).toString("base64url");
await db.resets.insert({
userId: user.id,
tokenHash: createHash("sha256").update(token).digest("hex"),
expiresAt: new Date(Date.now() + 15 * 60 * 1000),
usedAt: null,
});
await mail.send(user.email, `${APP_URL}/reset?token=${token}`);
}
res.json({ ok: true }); // même réponse dans tous les cas
});Pour les JWT, impose l'algorithme à la vérification au lieu de le lire dans le jeton :
// CORRIGÉ : la liste d'algorithmes acceptés est fixée par le serveur
const payload = jwt.verify(token, publicKey, {
algorithms: ["RS256"],
audience: "api.acme.test",
issuer: "https://auth.acme.test",
});Défense en profondeur : rotation de l'identifiant de session à la connexion et à l'élévation de privilège, invalidation de toutes les sessions au changement de mot de passe, limitation par compte des tentatives 2FA avec invalidation du code, confirmation par le mot de passe actuel pour les changements sensibles, notification e-mail à chaque changement de sécurité.
Erreurs qui font rejeter un rapport
- Rapporter l'énumération d'utilisateurs ou l'absence de rate-limit sans impact, alors que la politique les exclut.
- Présenter une prise de contrôle sans l'avoir démontrée sur son propre second compte.
- Annoncer « JWT vulnérable » parce que la charge utile est lisible : c'est le fonctionnement normal d'un JWT.
- Signaler un contournement 2FA qui exige déjà l'accès à la boîte mail ou au téléphone de la victime.
- Classer Critique un empoisonnement du
Hostqui exige un clic de la victime sans le mentionner. - Avoir verrouillé des comptes ou envoyé des centaines d'e-mails de reset pendant les tests.
Checklist du hunter
- Le lien de reset dépend-il de
Hostou d'un en-tête de réécriture ? - Le jeton est-il long, aléatoire, à usage unique, de courte durée, absent des réponses ?
- La 2FA bloque-t-elle réellement l'accès tant que le code n'est pas validé ? Est-elle liée à la session ?
- Les tentatives de code sont-elles limitées par compte ?
- L'identifiant de session change-t-il à la connexion ? Les sessions sont-elles invalidées au changement de mot de passe ?
- Le JWT a-t-il un
exp? L'algorithme est-il imposé côté serveur ? - Les chemins secondaires (API mobile, ancienne version, magic link) appliquent-ils les mêmes contrôles ?
- Ta preuve n'implique-t-elle que tes propres comptes ?