Retour au cours

cyber / cybersecurite-fondamentale

Mots de passe et authentification

Leçon 21 exercice

Explication

Ce que vous allez apprendre

  • Comprendre pourquoi un mot de passe ne doit jamais être stocké en clair, même chiffré
  • Expliquer pourquoi sha256 seul est un mauvais choix pour hacher un mot de passe, contrairement à un fichier
  • Utiliser un algorithme lent (bcrypt, scrypt ou argon2) pour stocker un mot de passe en toute sécurité
  • Reconnaître les grandes familles d'attaques (brute force, dictionary, credential stuffing, password spraying)
  • Mettre en place un rate limiting basique sur une route de connexion pour freiner le brute force

Dans quel contexte ?

Une équipe backend découvre qu'une table users de sa base de données a fuité lors d'un audit de sécurité. Si les mots de passe y étaient stockés avec un simple sha256, un attaquant équipé d'une carte graphique grand public peut en retrouver des milliers en quelques heures. S'ils étaient hachés avec bcrypt ou argon2, la même opération prendrait des années, ce qui change complètement la gravité de l'incident à déclarer.

Première étape : pourquoi on ne stocke jamais un mot de passe tel quel. Si une base de données fuite un jour (et ça arrive), le mot de passe brut de chaque utilisateur serait immédiatement lisible. La solution consiste à ne stocker qu'une empreinte (un hash), une sorte de résumé à sens unique qu'on ne peut pas "dérouler" pour retrouver l'original.

Deuxième étape : pourquoi sha256 ne suffit pas. On pourrait croire qu'un algorithme de hachage rapide suffit. C'est faux, et c'est le point le plus contre-intuitif de la leçon : sha256 est justement TROP rapide. Un attaquant qui vole la base peut tester des milliards de mots de passe par seconde avec une carte graphique.

Troisième étape : la solution, des algorithmes volontairement lents. bcrypt, scrypt et argon2 sont conçus pour être lents et gourmands en mémoire. Pour un utilisateur qui se connecte une fois, la lenteur est invisible. Pour un attaquant qui doit tester des milliards de combinaisons, elle devient économiquement absurde.

Quatrième étape : connaître les attaques pour mieux s'en défendre. Il existe plusieurs familles d'attaques sur les mots de passe, chacune avec sa propre logique.

AttaquePrincipeParade principale
Brute forceTester toutes les combinaisons possiblesHachage lent (bcrypt/argon2) + rate limiting
Dictionary attackTester des mots de passe réels déjà connusRejeter les mots de passe présents dans des fuites (HaveIBeenPwned)
Credential stuffingRéutiliser des couples email/mot de passe volés ailleursNe jamais réutiliser un mot de passe entre sites
Password sprayingTester UN mot de passe sur des milliers de comptesDétection d'anomalie sur les échecs groupés

Cinquième étape : ce qui a changé dans les recommandations. Pendant longtemps, on imposait majuscule + chiffre + symbole. Résultat : les utilisateurs produisaient des motifs prévisibles comme "P@ssw0rd1". Les recommandations modernes (NIST SP 800-63B) misent plutôt sur la longueur : un mot de passe long, même simple, est mathématiquement bien plus dur à casser qu'un mot de passe court et compliqué.

Piège fréquent

Croire qu'un mot de passe "compliqué" est forcément sûr. Un mot de passe complexe mais réutilisé sur trois sites différents est plus dangereux qu'un mot de passe long utilisé nulle part ailleurs.

Bonne pratique

Toujours choisir argon2id ou bcrypt (coût 12 minimum) pour hacher un mot de passe côté serveur, et vérifier les nouveaux mots de passe contre une liste de fuites connues via l'API HaveIBeenPwned en k-anonymity, sans jamais envoyer le mot de passe en clair sur le réseau.

Il reste cependant un problème de fond : même le meilleur mot de passe du monde reste un facteur unique, donc un point de défaillance unique. S'il fuite une seule fois, la porte est ouverte. La prochaine leçon ajoute un deuxième facteur, d'une nature complètement différente, pour combler cette fragilité.

Commandes & code

Mots de passe et authentification

Le mot de passe reste le facteur d'authentification le plus répandu et le plus attaqué.

text
# Attaques courantes sur les mots de passe
Brute force        : tester toutes les combinaisons possibles
Dictionary attack   : tester une liste de mots de passe courants (rockyou.txt, etc.)
Credential stuffing : réutiliser des paires email/mot de passe issues de fuites (breaches) sur d'autres sites
Password spraying   : tester UN mot de passe courant sur BEAUCOUP de comptes (évite le lockout par compte)
Rainbow table       : table précalculée de hachages pour retrouver un mot de passe en clair
python
# Mauvaise pratique : stocker le mot de passe en clair ou avec un hash faible
password_bad = "monMotDePasse123"  # JAMAIS stocker ceci tel quel en base

# Bonne pratique : hachage avec sel, avec un algorithme lent conçu pour les mots de passe
# bcrypt, scrypt ou argon2 - PAS md5/sha1/sha256 seuls (trop rapides = brute-forçables)
import bcrypt

def hash_password(plain_password: str) -> bytes:
    salt = bcrypt.gensalt(rounds=12)  # coût ajustable, 12 = bon compromis 2024
    return bcrypt.hashpw(plain_password.encode("utf-8"), salt)

def verify_password(plain_password: str, hashed: bytes) -> bool:
    return bcrypt.checkpw(plain_password.encode("utf-8"), hashed)

hashed = hash_password("Tr0ub4dor&3")
assert verify_password("Tr0ub4dor&3", hashed) is True
assert verify_password("mauvais_mdp", hashed) is False
python
# Encore mieux : argon2id (vainqueur du Password Hashing Competition), résistant GPU/ASIC
from argon2 import PasswordHasher

ph = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=4)  # 64 Mo de RAM par hash
hashed = ph.hash("Tr0ub4dor&3")

try:
    ph.verify(hashed, "Tr0ub4dor&3")
    print("mot de passe valide")
except Exception:
    print("mot de passe invalide")
python
# Politique de mot de passe moderne (NIST SP 800-63B) : privilégier la LONGUEUR
# plutôt que la complexité imposée (majuscule+chiffre+symbole = mots de passe prévisibles "P@ssw0rd1")
import re

def is_password_acceptable(pw: str, breached_list: set[str]) -> tuple[bool, str]:
    if len(pw) < 12:
        return False, "au moins 12 caractères recommandés"
    if pw.lower() in breached_list:  # vérifier contre une liste de mots de passe compromis (HaveIBeenPwned)
        return False, "ce mot de passe est présent dans des fuites connues"
    if re.fullmatch(r"(.)+", pw):  # rejette "aaaaaaaaaaaa"
        return False, "mot de passe trop simple"
    return True, "ok"
bash
# Vérifier un mot de passe contre HaveIBeenPwned SANS jamais envoyer le mot de passe en clair
# (k-anonymity: on envoie seulement les 5 premiers caractères du hash SHA1)
PASSWORD="Tr0ub4dor&3"
HASH=$(printf '%s' "$PASSWORD" | sha1sum | tr 'a-z' 'A-Z' | cut -c1-40)
PREFIX=${HASH:0:5}
SUFFIX=${HASH:5}
curl -s "https://api.pwnedpasswords.com/range/$PREFIX" | grep -i "$SUFFIX"
text
# Protection contre le brute force côté application
1. Rate limiting sur /login (ex: 5 tentatives / 15 min / IP+compte)
2. Verrouillage progressif du compte (backoff exponentiel)
3. CAPTCHA après N échecs
4. Alerte / notification sur connexion depuis un nouvel appareil ou pays
5. Journalisation des échecs d'authentification pour détection d'anomalie
python
# Exemple simplifié de rate limiting sur une route de login (FastAPI + dépendance mémoire/Redis)
from fastapi import FastAPI, HTTPException, Request
from collections import defaultdict
from time import time

app = FastAPI()
attempts: dict[str, list[float]] = defaultdict(list)
WINDOW_SECONDS = 900  # 15 minutes
MAX_ATTEMPTS = 5

@app.post("/login")
async def login(request: Request):
    ip = request.client.host
    now = time()
    attempts[ip] = [t for t in attempts[ip] if now - t < WINDOW_SECONDS]
    if len(attempts[ip]) >= MAX_ATTEMPTS:
        raise HTTPException(status_code=429, detail="Trop de tentatives, réessayez plus tard")
    attempts[ip].append(now)
    # ... vérification des identifiants ...

Résumé

  • Ne jamais stocker un mot de passe en clair ni avec sha256 seul : utiliser bcrypt/scrypt/argon2.
  • La longueur prime sur la complexité forcée (NIST SP 800-63B).
  • Vérifier les mots de passe contre des listes de fuites connues (k-anonymity API).
  • Le rate limiting et le verrouillage progressif sont indispensables contre le brute force.

Exercices pratiques

1 disponible
1

Mission : auditer une table users qui a fuité

Objectif : Évaluer la vraie gravité d'une fuite de mots de passe selon l'algorithme de hachage utilisé, et corriger la faille de stockage.

Contexte

Un audit externe révèle qu'une table users de Technologik a fuité il y a six mois. Chaque mot de passe est stocké sous la forme d'une chaîne hexadécimale de 64 caractères, identique à chaque connexion pour un même mot de passe, et il n'existe aucune colonne salt dans le schéma.

Résoudre l’exercice →