Une base fuit, et les mots de passe avec
Un site se fait pirater et sa table users se retrouve en ligne. Ça arrive tout le temps. Dedans, une ligne ressemble à ça :
bob | 5f4dcc3b5aa765d61d8327deb882cf99
Le mot de passe n'est pas en clair, il est haché en md5. Rassurant ? Pas du tout. Collez ce hash dans un moteur de recherche : vous tombez sur password en une seconde. Partout, des tables toutes prêtes associent déjà chaque mot de passe courant à son md5. L'attaquant ne « casse » même pas le hash, il le cherche.
Et le pire vient après. Bob réutilise sûrement ce mot de passe ailleurs : sa boîte mail, sa banque. L'attaquant rejoue le couple bob@email / password sur cent autres sites (c'est le credential stuffing). La fuite d'un petit site mal protégé vide des comptes partout. Tout ça commence par un mot de passe mal stocké.
Cette leçon parle de l'authentification (prouver qui on est) et de sa pièce la plus sensible : le mot de passe. C'est lié à A07 de l'OWASP 2025 (Authentication Failures). La leçon précédente vérifiait si vous aviez le droit de faire une action (le contrôle d'accès) : celle-ci répond d'abord à la question en amont, « qui êtes-vous ? ».
Pourquoi md5 (et sha256) est une faute
Un mot de passe haché avec md5 ou sha1 a deux problèmes, et ils sont tous les deux graves.
1. C'est trop rapide. md5 a été conçu pour hacher des fichiers à toute vitesse. Sur une carte graphique, on calcule des milliards de md5 par seconde. Du coup, on teste tous les mots de passe possibles très vite. Un mot de passe court tombe en quelques minutes. Un mot courant tombe instantanément : son md5 est déjà dans une table toute prête. Pour un mot de passe, la vitesse est l'ennemi.
2. Il n'y a pas de sel. Deux personnes avec le même mot de passe obtiennent le même hash. Regardez bob et chloé dans le labo plus bas : hash identique, donc mot de passe identique, ça se voit d'un coup d'œil. Surtout, ça permet une seule table précalculée (mot de passe → hash) qui craque tout le monde d'un coup. C'est ce qu'on appelle une rainbow table.
La conclusion : un bon hachage de mot de passe doit être l'inverse de md5, lent et unique pour chaque utilisateur.
Le faux remède : « je passe en sha256, c'est plus moderne » ne change rien. sha256 est lui aussi un hash rapide (fait pour la vitesse), donc tout aussi cassable hors ligne. Et « je fais md5 deux fois » ou « j'invente mon propre sel maison » non plus : on ne bricole pas sa crypto. La bonne réponse est une fonction faite pour les mots de passe (section 4).
À vous d'attaquer : crackez la table volée
Vous venez de voler la table users d'un site. Les mots de passe sont en md5. Lancez l'attaque par dictionnaire : l'outil compare chaque hash à une table de mots de passe courants déjà calculée. Puis basculez le stockage en bcrypt et relancez : la même attaque ne donne plus rien. Tout tourne dans votre navigateur, rien n'est envoyé.
Ce qui vient de se passer
En md5, quatre comptes tombent instantanément : leur mot de passe était courant, donc déjà dans la table précalculée. Notez que bob et chloé ont exactement le même hash : ils ont le même mot de passe, et l'absence de sel le révèle. Seul thomas résiste : son mot de passe est long et unique, absent du dictionnaire (mais en md5, il finirait par tomber au brute force).
En bcrypt, la même attaque trouve zéro. Deux raisons : bcrypt est lent (chaque essai prend du temps, des milliards/seconde deviennent quelques milliers), et il est salé (bob et chloé ont maintenant des hashes différents, la table précalculée ne sert plus à rien).
Le correctif : une fonction faite pour les mots de passe
La solution est d'utiliser une fonction conçue pour hacher les mots de passe : bcrypt, Argon2 (la version Argon2id de préférence) ou scrypt. Elles font deux choses que md5 ne fait pas. Elles sont lentes et réglables (un « coût » que vous montez avec les années, à mesure que le matériel s'améliore). Et elles salent toutes seules : un sel aléatoire par mot de passe, rangé dans le hash lui-même. Plus de rainbow table, et deux mots de passe identiques donnent deux hashes différents.
En PHP, c'est intégré et tient en deux fonctions. À l'inscription, on hache ; à la connexion, on vérifie :
// inscription : le sel et le coût sont gérés pour vous
$hash = password_hash($motDePasse, PASSWORD_DEFAULT); // bcrypt (ou PASSWORD_ARGON2ID)
// … on range $hash en base, jamais le mot de passe …
// connexion : on compare la saisie au hash stocké
if (password_verify($saisie, $hash)) {
// mot de passe correct
}
On ne range jamais le mot de passe en clair, et jamais un md5. password_hash et password_verify s'occupent du sel, du coût et du format. La fonction password_needs_rehash() permet même de re-hacher au passage pour monter le coût plus tard.
En Go, c'est la même idée avec le paquet bcrypt :
// inscription
hash, _ := bcrypt.GenerateFromPassword([]byte(motDePasse), bcrypt.DefaultCost)
// connexion : nil = mot de passe correct
err := bcrypt.CompareHashAndPassword(hash, []byte(saisie))
Le piège de la politique de mot de passe. Les règles « 1 majuscule, 1 chiffre, 1 symbole » ne rendent pas plus sûr : elles poussent à P@ssw0rd, faible et prévisible. Ce qui compte, c'est la longueur. Visez des phrases de passe, autorisez les mots de passe très longs, autorisez le copier-coller (donc les gestionnaires de mots de passe), et ne forcez pas un changement tous les 3 mois (ça produit Été2026! puis Été2026!!). Référence : NIST 800-63B.
Défense en profondeur :
- bcrypt ou Argon2id pour stocker (lent + salé), jamais md5/sha1/clair ;
- longueur avant complexité : phrases de passe, autoriser le très long et le copier-coller ;
- refuser les mots de passe déjà fuités (comparaison à une liste type Have I Been Pwned) ;
- message d'erreur générique (« identifiants invalides ») pour ne pas dire si le compte existe ;
- activer le MFA (section plus bas), la vraie barrière quand un mot de passe fuite quand même.
Référence : OWASP Password Storage Cheat Sheet.
La méthode et l'arsenal du pentester
Maintenant qu'on sait comment défendre, voyons comment un attaquant s'y prend concrètement.
On distingue deux façons d'attaquer un mot de passe.
Hors ligne (la base a fuité). On a les hashes, on essaie des milliards de mots de passe sur sa propre machine, sans limite. C'est ce qu'on a simulé. En md5, c'est instantané ; en bcrypt, c'est si lent que l'attaque devient rarement rentable.
En ligne (on tape au portail de connexion). Pas de fuite, on essaie des mots de passe directement sur le formulaire. Trois variantes : le brute force (tout essayer sur un compte), le password spraying (un seul mot de passe courant testé sur des milliers de comptes : un seul essai par compte, donc rien ne se bloque), et le credential stuffing (rejouer des couples email/mot de passe d'anciennes fuites).
Un détail qui aide l'attaquant : si « utilisateur inconnu » et « mot de passe incorrect » donnent deux messages différents, on peut lister les comptes valides (énumération). D'où le message unique.
L'arsenal.
- hashcat / John the Ripper : le cassage hors ligne sur GPU, avec des dictionnaires (le fameux
rockyou.txt) et des règles de mutation (password→P4ssw0rd!). - hydra : le brute force en ligne de formulaires et services (SSH, FTP, login web).
- Burp Intruder : fuzzer un formulaire de login, tester du credential stuffing, repérer l'énumération d'utilisateurs.
- Have I Been Pwned : vérifier si un mot de passe ou un email traîne déjà dans une fuite connue.
Côté défense en ligne : limiter le débit (rate limiting), ralentir progressivement après chaque échec, un CAPTCHA au bout de N essais, des alertes. Et surtout le MFA. Attention au verrouillage de compte trop strict : c'est une arme de déni de service (on bloque les comptes des autres exprès).
Le vrai filet : MFA et passkeys
Même bien haché, un mot de passe reste volable : par phishing, par une fuite, par réutilisation. La parade est d'ajouter un deuxième facteur (MFA, ou 2FA). L'idée : combiner quelque chose qu'on sait (le mot de passe), quelque chose qu'on a (un téléphone, une clé) et parfois quelque chose qu'on est (empreinte, visage). Voler le mot de passe ne suffit plus.
Les formes, de la moins à la plus solide :
- SMS : un code par texto. Mieux que rien, mais faible (on peut détourner un numéro par SIM swap, ou intercepter le SMS).
- TOTP : le code à 6 chiffres d'une appli (Google Authenticator, Aegis…). Bon et simple. Reste piégeable par phishing : un faux site peut demander le code et le rejouer aussitôt.
- Passkeys / WebAuthn : le standard moderne, résistant au phishing. La clé est liée au vrai domaine, donc un faux site ne peut pas s'en servir. C'est la direction que prennent Google, Apple et Microsoft.
Côté développeur : proposez au minimum le TOTP, visez les passkeys. Et re-demandez le mot de passe ou le deuxième facteur pour les actions sensibles (changer l'email, supprimer le compte).
Ce que ça révèle côté défense : aucune couche ne suffit seule. Un bon stockage (bcrypt/Argon2) protège la base si elle fuite. Le rate limiting freine les attaques en ligne. Le MFA sauve le compte quand le mot de passe a quand même fuité. Et la longueur du mot de passe fait tout le reste du travail. On empile (leçon 2).
La checklist authentification
À garder sous les yeux à chaque fois que vous codez un login.
- Stockage :
password_hash(bcrypt ou Argon2id). Jamais md5, sha1, ni mot de passe en clair. - Vérification :
password_verify, et un message d'erreur unique (« identifiants invalides »). - Politique : longueur avant complexité, autoriser le très long et le copier-coller, pas de rotation forcée, refuser les mots de passe déjà fuités.
- Anti-brute force : rate limiting, ralentissement progressif, CAPTCHA après N essais, alertes.
- Deuxième facteur : TOTP au minimum, passkeys comme cible, SMS en dernier recours.
Les références. Le NIST 800-63B est la référence moderne des règles de mot de passe (longueur, pas de rotation forcée, vérifier les fuites). Pour s'entraîner légalement : les labs « Authentication » de la Web Security Academy (brute force, énumération, contournement du 2FA).
Rappel. Tester des mots de passe sur un compte qui n'est pas le vôtre, c'est une attaque réelle, même « pour voir ». On ne le fait que sur ses propres systèmes ou une cible explicitement autorisée (leçon 1).
A database leaks, and the passwords with it
A site gets hacked and its users table ends up online. It happens all the time. Inside, one row looks like this:
bob | 5f4dcc3b5aa765d61d8327deb882cf99
The password isn't in clear text, it's hashed with md5. Reassuring? Not at all. Paste that hash into a search engine: you land on password in one second. Everywhere, ready-made tables already map every common password to its md5. The attacker doesn't even "crack" the hash, they look it up.
And the worst comes next. Bob surely reuses this password elsewhere: his email, his bank. The attacker replays the pair bob@email / password on a hundred other sites (that's credential stuffing). The breach of one poorly-protected small site empties accounts everywhere. It all starts with a badly stored password.
This lesson is about authentication (proving who you are) and its most sensitive piece: the password. It maps to A07 in OWASP 2025 (Authentication Failures). The previous lesson checked whether you had the right to perform an action (access control): this one answers the prior question, "who are you?".
Why md5 (and sha256) is a mistake
A password hashed with md5 or sha1 has two problems, and both are serious.
1. It's too fast. md5 was built to hash files at high speed. On a graphics card, you compute billions of md5 per second. So you test every possible password very fast. A short password falls in minutes. A common one falls instantly: its md5 is already in a ready-made table. For a password, speed is the enemy.
2. There's no salt. Two people with the same password get the same hash. Look at bob and chloé in the lab below: identical hash, so identical password, you can see it at a glance. Above all, it allows a single precomputed table (password → hash) that cracks everyone at once. That's called a rainbow table.
The takeaway: a good password hash must be the opposite of md5, slow and unique for each user.
Beware the false cure: "I'll switch to sha256, it's more modern" changes nothing. sha256 is also a fast hash (built for speed), so just as crackable offline. And "I'll md5 it twice" or "I'll invent my own homemade salt" don't help either: you don't roll your own crypto. The right answer is a function made for passwords (section 4).
Your turn to attack: crack the stolen table
You've just stolen a site's users table. The passwords are in md5. Run the dictionary attack: the tool compares each hash to an already-computed table of common passwords. Then switch storage to bcrypt and run again: the same attack finds nothing. Everything runs in your browser, nothing is sent.
What just happened
In md5, four accounts fall instantly: their password was common, so already in the precomputed table. Notice that bob and chloé have the exact same hash: they share a password, and the lack of salt reveals it. Only thomas resists: his password is long and unique, absent from the dictionary (but in md5, it would eventually fall to brute force).
In bcrypt, the same attack finds zero. Two reasons: bcrypt is slow (each try takes time, billions/second become a few thousand), and it's salted (bob and chloé now have different hashes, the precomputed table is useless).
The fix: a function made for passwords
The solution is to use a function designed to hash passwords: bcrypt, Argon2 (Argon2id preferably) or scrypt. They do two things md5 doesn't. They are slow and adjustable (a "cost" you raise over the years as hardware improves). And they salt automatically: a random salt per password, stored inside the hash itself. No more rainbow tables, and two identical passwords give two different hashes.
In PHP, it's built in and fits in two functions. At sign-up, you hash; at login, you verify:
// sign-up: salt and cost are handled for you
$hash = password_hash($password, PASSWORD_DEFAULT); // bcrypt (or PASSWORD_ARGON2ID)
// … store $hash in the database, never the password …
// login: compare the input to the stored hash
if (password_verify($input, $hash)) {
// password correct
}
You never store the password in clear text, and never a md5. password_hash and password_verify handle the salt, the cost and the format. The password_needs_rehash() function even lets you re-hash later to raise the cost.
In Go, same idea with the bcrypt package:
// sign-up
hash, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
// login: nil = password correct
err := bcrypt.CompareHashAndPassword(hash, []byte(input))
The password-policy trap. Rules like "1 uppercase, 1 digit, 1 symbol" don't make things safer: they push people to P@ssw0rd, weak and predictable. What matters is length. Aim for passphrases, allow very long passwords, allow paste (so password managers), and don't force a change every 3 months (it produces Summer2026! then Summer2026!!). Reference: NIST 800-63B.
Defense in depth:
- bcrypt or Argon2id for storage (slow + salted), never md5/sha1/plaintext;
- length over complexity: passphrases, allow very long passwords and paste;
- reject already-breached passwords (check against a list like Have I Been Pwned);
- generic error message ("invalid credentials") so you don't reveal whether the account exists;
- enable MFA (section below), the real barrier when a password leaks anyway.
Reference: OWASP Password Storage Cheat Sheet.
The pentester's method and arsenal
Now that we know how to defend, let's see how an attacker goes about it in practice.
There are two ways to attack a password.
Offline (the database leaked). You have the hashes, so you try billions of passwords on your own machine, with no limit. That's what we simulated. In md5, it's instant; in bcrypt, it's so slow that the attack is rarely worth it.
Online (you hit the login form). No leak, you try passwords directly on the form. Three variants: brute force (try everything on one account), password spraying (one common password tested on thousands of accounts: one try per account, so nothing gets locked), and credential stuffing (replaying email/password pairs from past breaches).
One detail that helps the attacker: if "unknown user" and "wrong password" give two different messages, you can list the valid accounts (enumeration). Hence the single message.
The arsenal.
- hashcat / John the Ripper: offline cracking on GPU, with dictionaries (the famous
rockyou.txt) and mutation rules (password→P4ssw0rd!). - hydra: online brute force of forms and services (SSH, FTP, web login).
- Burp Intruder: fuzz a login form, test credential stuffing, spot user enumeration.
- Have I Been Pwned: check whether a password or email already sits in a known breach.
On the online-defense side: rate limiting, a progressive slowdown after each failure, a CAPTCHA after N tries, alerts. And above all MFA. Watch out for overly strict account lockout: it's a denial-of-service weapon (you lock other people's accounts on purpose).
The real safety net: MFA and passkeys
Even well hashed, a password stays stealable: by phishing, by a leak, by reuse. The fix is to add a second factor (MFA, or 2FA). The idea: combine something you know (the password), something you have (a phone, a key) and sometimes something you are (fingerprint, face). Stealing the password is no longer enough.
The forms, from weakest to strongest:
- SMS: a code by text. Better than nothing, but weak (a number can be hijacked by SIM swap, or the SMS intercepted).
- TOTP: the 6-digit code from an app (Google Authenticator, Aegis…). Good and simple. Still phishable: a fake site can ask for the code and replay it at once.
- Passkeys / WebAuthn: the modern standard, phishing-resistant. The key is bound to the real domain, so a fake site can't use it. That's the direction Google, Apple and Microsoft are taking.
As a developer: offer TOTP at the very least, aim for passkeys. And re-ask the password or the second factor for sensitive actions (change the email, delete the account).
What this reveals for defense: no single layer is enough. Good storage (bcrypt/Argon2) protects the database if it leaks. Rate limiting slows online attacks. MFA saves the account when the password leaked anyway. And password length does all the rest of the work. You stack them (lesson 2).
The authentication checklist
Keep this in front of you every time you code a login.
- Storage:
password_hash(bcrypt or Argon2id). Never md5, sha1, or plaintext. - Verification:
password_verify, and a single error message ("invalid credentials"). - Policy: length over complexity, allow very long passwords and paste, no forced rotation, reject already-breached passwords.
- Anti-brute force: rate limiting, progressive slowdown, CAPTCHA after N tries, alerts.
- Second factor: TOTP at minimum, passkeys as the target, SMS as a last resort.
The references. NIST 800-63B is the modern reference for password rules (length, no forced rotation, check breaches). To practice legally: the "Authentication" labs of the Web Security Academy (brute force, enumeration, 2FA bypass).
Reminder. Testing passwords on an account that isn't yours is a real attack, even "just to see". Only do it on your own systems or an explicitly authorized target (lesson 1).
Un dev stocke les mots de passe avec md5($pw). Un autre avec password_hash($pw, PASSWORD_DEFAULT). Avant de dérouler : après une fuite de la base, avec lequel l'attaquant retrouve-t-il password en une seconde ? Et que voit-il si deux utilisateurs ont le même mot de passe ?
Voir la réponse
Avec md5 : instantané. Le hash de password est connu, rangé dans des tables précalculées. Et comme md5 n'a pas de sel, deux utilisateurs avec le même mot de passe ont le même hash : l'attaquant le voit d'un coup d'œil. Avec password_hash (bcrypt) : l'attaque hors ligne est trop lente pour être rentable, et le sel rend chaque hash unique, donc deux mots de passe identiques donnent deux hashes différents. La table précalculée ne sert plus à rien.
One dev stores passwords with md5($pw). Another with password_hash($pw, PASSWORD_DEFAULT). Before you expand: after a database leak, with which one does the attacker recover password in one second? And what do they see if two users share a password?
Show the answer
With md5: instant. The hash of password is known, sitting in precomputed tables. And since md5 has no salt, two users with the same password get the same hash: the attacker sees it at a glance. With password_hash (bcrypt): the offline attack is too slow to be worth it, and the salt makes each hash unique, so two identical passwords give two different hashes. The precomputed table is useless.
🎯 Pratique
S'entraîner (clique pour ouvrir) :
💬 Ré-explique sans regarder
Avec tes mots : pourquoi un hash de mot de passe doit-il être lent et salé, et pourquoi sha256 (pourtant « solide ») ne convient pas pour stocker un mot de passe ?
🧠 Rappel libre
Sans remonter : donne les deux raisons pour lesquelles md5 est une faute pour les mots de passe, explique ce qu'apporte le sel, et dis à quoi sert le MFA quand un mot de passe a quand même fuité.
⚖️ Juge le code de l'IA
Tu demandes à l'IA de stocker les mots de passe correctement. Elle répond : « J'utilise sha256(motDePasse + sel) avec un sel aléatoire de 32 octets par utilisateur, rangé à côté du hash. C'est salé, donc les rainbow tables ne marchent plus : c'est sûr. » Tu acceptes, ou tu rejettes ?
password_hash (bcrypt) ou Argon2id, qui gèrent le sel et la lenteur. On ne bricole pas sha256 + sel à la main.md5. Combien de temps pour retrouver un mot de passe courant comme password ?sha256(motDePasse + sel) avec un sel aléatoire par utilisateur. Pourquoi ce n'est toujours pas suffisant ?Password verified, the user is logged in. But how does the server remember that on the next request? Lesson 8 opens up sessions and cookies: what really travels, and how a session gets stolen.
Lesson 8: Secure sessions & cookies →