Stocker un mot de passe en clair, ou avec md5(), c’est exposer tous tes utilisateurs le jour où ta base de données fuite. PHP fournit pourtant tout ce qu’il faut, en natif : password_hash(), password_verify() et password_needs_rehash(). Voici comment les utiliser correctement, avec un exemple complet d’inscription et de connexion.

💡 Bonne pratique🐘 PHP 8.4🔐 bcrypt · Argon2id⏱️ 13 min de lecture

La façon dont tu stockes un mot de passe est l’une des décisions de sécurité les plus importantes d’une application web. Une base de données peut fuiter pour mille raisons : une injection SQL, une sauvegarde oubliée sur un serveur, un accès volé à l’hébergement. Ce jour-là, la seule chose qui protège tes utilisateurs, c’est la manière dont leurs mots de passe ont été enregistrés.

Bonne nouvelle : en PHP, faire les choses bien est plus simple que de les faire mal. Trois fonctions natives suffisent, sans bibliothèque externe. Ce guide t’explique pourquoi les anciennes méthodes sont dangereuses, comment utiliser password_hash() pas à pas, et comment migrer une base existante sans forcer tes utilisateurs à changer de mot de passe.

Stocker un mot de passe en PHP avec password_hash et password_verify
Stocker un mot de passe en PHP : trois fonctions natives suffisent.
⚠️
En une phrase

Enregistre chaque mot de passe avec password_hash($motDePasse, PASSWORD_DEFAULT), stocke le résultat dans une colonne VARCHAR(255), vérifie à la connexion avec password_verify(), et n’utilise jamais md5(), sha1() ou un chiffrement réversible.

Pourquoi on ne stocke jamais un mot de passe en clair

Stocker un mot de passe en clair, c’est écrire dans ta base exactement ce que l’utilisateur a tapé. En cas de fuite, l’attaquant obtient immédiatement tous les identifiants. Et comme beaucoup de personnes réutilisent le même mot de passe partout, la fuite de ton petit site peut ouvrir l’accès à leur messagerie, leur banque ou leurs réseaux sociaux.

La solution consiste à stocker non pas le mot de passe, mais une empreinte (un hash) calculée à partir de lui. Une fonction de hachage transforme le mot de passe en une chaîne de caractères impossible à « décoder ». À la connexion, on recalcule l’empreinte du mot de passe saisi et on compare.

MéthodeRéversible ?Rapide à calculer ?Verdict
————
Texte en clairOui—❌ À proscrire absolument
Chiffrement (AES…)Oui, avec la cléOui❌ Si la clé fuite, tout fuite
md5(), sha1(), sha256NonOui, extrêmement❌ Cassable par force brute
password_hash() (bcrypt, Argon2id)NonNon, volontairement lent✅ La bonne méthode

Le point clé est dans la dernière colonne : un bon algorithme de hachage de mot de passe est volontairement lent. Pour un utilisateur qui se connecte, attendre quelques dizaines de millisecondes ne change rien. Pour un attaquant qui veut tester des milliards de combinaisons, cette lenteur rend l’attaque irréaliste.

Pourquoi md5() et sha1() ne protègent pas un mot de passe

md5() et sha1() sont des fonctions de hachage, mais elles ont été conçues pour être rapides : vérifier l’intégrité d’un fichier, comparer des données. Une carte graphique moderne peut calculer des milliards d’empreintes MD5 par seconde. Un mot de passe de huit caractères y résiste quelques minutes, parfois moins.

Il existe aussi des tables arc-en-ciel : d’immenses bases d’empreintes précalculées. L’empreinte MD5 de « motdepasse123 » s’y trouve depuis longtemps, et il suffit de la chercher pour retrouver le mot de passe d’origine.

<?php
// ❌ À NE JAMAIS FAIRE
$hash = md5($_POST['password']);
$hash = sha1($_POST['password']);
$hash = hash('sha256', $_POST['password']);

// Deux utilisateurs avec le même mot de passe ont la même empreinte :
// une seule recherche révèle les deux.

Ajouter un « sel » fait maison (md5($sel . $motDePasse)) améliore un peu les choses, mais ne règle pas le problème principal : la vitesse. C’est exactement ce que password_hash() corrige, en ajoutant automatiquement un sel unique et un coût de calcul réglable.

password_hash() : le mot de passe haché correctement

La fonction password_hash() existe depuis PHP 5.5 et fait tout le travail : elle génère un sel aléatoire unique, applique un algorithme lent conçu pour les mots de passe, et renvoie une chaîne qui contient tout ce qu’il faut pour vérifier le mot de passe plus tard.

<?php
$motDePasse = 'MonSuperMotDePasse!2026';

$hash = password_hash($motDePasse, PASSWORD_DEFAULT);

echo $hash;
// Exemple de résultat (il change à chaque appel) :
// $2y$12$Qm9vR3lYb2pTZ2V0V2hhdGV2ZXIuaGFzaGVkLnZhbHVlLmhlcmUuX3g

Décortiquons ce résultat, car il est riche d’informations :

  • $2y$ : l’algorithme utilisé, ici bcrypt.
  • 12$ : le coût, c’est-à-dire la lenteur volontaire. Depuis PHP 8.4, le coût par défaut de bcrypt est passé de 10 à 12.
  • les 22 caractères suivants : le sel, généré aléatoirement à chaque appel.
  • le reste : l’empreinte proprement dite.

Conséquence importante : si tu appelles deux fois password_hash() avec le même mot de passe, tu obtiens deux résultats différents, grâce au sel. Tu ne peux donc pas comparer deux empreintes entre elles : il faut toujours passer par password_verify().

🧠
Pourquoi PASSWORD_DEFAULT ?

PASSWORD_DEFAULT désigne l’algorithme recommandé par PHP, aujourd’hui bcrypt. Il peut évoluer dans une future version. En l’utilisant, ton application profite automatiquement des améliorations, à condition de prévoir une colonne assez grande pour stocker chaque mot de passe haché.

password_verify() : vérifier le mot de passe à la connexion

À la connexion, tu récupères l’empreinte stockée et tu la compares au mot de passe saisi avec password_verify(). La fonction lit l’algorithme, le coût et le sel directement dans l’empreinte : tu n’as rien d’autre à lui fournir.

<?php
$saisi = $_POST['password'] ?? '';
$hashEnBase = $utilisateur['password_hash'];

if (password_verify($saisi, $hashEnBase)) {
    // Mot de passe correct
} else {
    // Mot de passe incorrect
}

password_verify() effectue une comparaison à temps constant : elle prend toujours le même temps, que le mot de passe soit presque bon ou complètement faux. Cela empêche les attaques qui mesurent le temps de réponse pour deviner l’empreinte caractère par caractère. Ne remplace donc jamais cette fonction par une comparaison maison avec == ou ===.

Cycle de vie d’un mot de passe en PHP : inscription, connexion et rehash
Le cycle complet d’un mot de passe : hachage à l’inscription, vérification à la connexion, mise à niveau automatique.

Exemple complet : inscription et connexion avec PDO

Voici un exemple complet et sécurisé, avec une table MySQL, une inscription et une connexion. Il utilise des requêtes préparées PDO pour éviter les injections SQL.

CREATE TABLE utilisateurs (
    id INT AUTO_INCREMENT PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL,
    cree_le DATETIME DEFAULT CURRENT_TIMESTAMP
);
<?php
// inscription.php
$pdo = new PDO('mysql:host=localhost;dbname=app;charset=utf8mb4', 'user', 'secret', [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);

$email = trim($_POST['email'] ?? '');
$motDePasse = $_POST['password'] ?? '';

if (!filter_var($email, FILTER_VALIDATE_EMAIL) || strlen($motDePasse) < 12) {
    exit('Adresse e-mail invalide ou mot de passe trop court (12 caractères minimum).');
}

$hash = password_hash($motDePasse, PASSWORD_DEFAULT);

$stmt = $pdo->prepare('INSERT INTO utilisateurs (email, password_hash) VALUES (?, ?)');
$stmt->execute([$email, $hash]);
<?php
// connexion.php
session_start();

$stmt = $pdo->prepare('SELECT id, password_hash FROM utilisateurs WHERE email = ?');
$stmt->execute([trim($_POST['email'] ?? '')]);
$utilisateur = $stmt->fetch(PDO::FETCH_ASSOC);

if ($utilisateur && password_verify($_POST['password'] ?? '', $utilisateur['password_hash'])) {
    session_regenerate_id(true);
    $_SESSION['utilisateur_id'] = $utilisateur['id'];
    header('Location: /tableau-de-bord.php');
    exit;
}

// Message volontairement vague : ne pas révéler si l'e-mail existe
echo 'Identifiants incorrects.';

Trois détails de sécurité méritent ton attention dans cet exemple :

  • La colonne fait 255 caractères. Une empreinte bcrypt fait 60 caractères aujourd’hui, mais un futur algorithme pourrait produire une chaîne plus longue.
  • session_regenerate_id(true) change l’identifiant de session après la connexion, ce qui bloque les attaques par fixation de session.
  • Le message d’erreur est identique que l’e-mail existe ou non, pour ne pas aider un attaquant à deviner les comptes existants.

Si tu as besoin de rafraîchir tes bases sur les requêtes et la structure d’une base, notre tutoriel application de vote électronique en PHP et MySQL met en pratique une authentification complète.

password_needs_rehash() : faire évoluer tes mots de passe

Les ordinateurs deviennent plus rapides chaque année, et le coût qui était suffisant il y a cinq ans ne l’est plus forcément aujourd’hui. password_needs_rehash() vérifie si une empreinte correspond encore aux réglages actuels. Si ce n’est pas le cas, on profite de la connexion (le seul moment où l’on connaît le mot de passe en clair) pour la recalculer.

<?php
if (password_verify($saisi, $utilisateur['password_hash'])) {

    if (password_needs_rehash($utilisateur['password_hash'], PASSWORD_DEFAULT)) {
        $nouveau = password_hash($saisi, PASSWORD_DEFAULT);
        $pdo->prepare('UPDATE utilisateurs SET password_hash = ? WHERE id = ?')
            ->execute([$nouveau, $utilisateur['id']]);
    }

    // Connexion réussie...
}

Avec ces quelques lignes, chaque mot de passe de ta base sera mis à niveau automatiquement lors de la prochaine connexion de son propriétaire, par exemple quand tu passes à PHP 8.4 et que le coût par défaut monte à 12.

bcrypt ou Argon2id : quel algorithme pour ton mot de passe ?

PHP propose deux familles d’algorithmes via password_hash().

bcrypt (PASSWORD_BCRYPT)Argon2id (PASSWORD_ARGON2ID)
———
DisponibilitéToujours disponibleSi PHP est compilé avec la bibliothèque Argon2
Réglagescost (défaut 12 depuis PHP 8.4)memory_cost, time_cost, threads
Point fortÉprouvé, universelRésiste mieux aux attaques par carte graphique grâce à la mémoire
LimiteN’utilise que les 72 premiers octets du mot de passePlus gourmand en mémoire serveur
Recommandation OWASPAcceptable, coût d’au moins 10Premier choix
<?php
// Vérifier si Argon2id est disponible sur ton serveur
var_dump(defined('PASSWORD_ARGON2ID'));

// L'utiliser avec les réglages par défaut de PHP
$hash = password_hash($motDePasse, PASSWORD_ARGON2ID);
// $argon2id$v=19$m=65536,t=4,p=1$...

En pratique, PASSWORD_DEFAULT (bcrypt) est un excellent choix pour la grande majorité des sites. Si ton hébergement propose Argon2id, c’est encore mieux. Dans les deux cas, ton code ne change pas : password_verify() reconnaît automatiquement l’algorithme de chaque empreinte.

Comparatif bcrypt et Argon2id pour hacher un mot de passe en PHP
bcrypt ou Argon2id : deux algorithmes sûrs pour protéger chaque mot de passe.

Régler le coût de bcrypt sur ton serveur

Le bon coût est le plus élevé possible tant que le hachage reste rapide pour l’utilisateur. La documentation PHP recommande de viser environ 350 millisecondes au maximum pour un serveur web interactif. Ce petit script, inspiré de la documentation officielle, trouve la bonne valeur pour ta machine :

<?php
// Trouver le coût optimal de bcrypt pour ce serveur
$objectif = 0.350; // 350 ms

$cout = 10;
do {
    $cout++;
    $debut = microtime(true);
    password_hash('test', PASSWORD_BCRYPT, ['cost' => $cout]);
    $duree = microtime(true) - $debut;
} while ($duree < $objectif);

echo "Coût recommandé : " . ($cout - 1);

Sur la plupart des serveurs actuels, le résultat se situe entre 11 et 13. Garder la valeur par défaut de PHP 8.4 (12) est un choix raisonnable si tu ne veux pas t’en occuper.

Migrer une ancienne base en MD5 sans réinitialiser les mots de passe

Tu reprends un vieux projet dont chaque mot de passe est stocké en MD5 ? Pas besoin de demander à tous les utilisateurs de le changer. Il existe une technique en deux temps.

Étape 1 : envelopper immédiatement les anciennes empreintes. On applique password_hash() sur l’empreinte MD5 existante. La base n’est plus exploitable en cas de fuite, sans attendre que les utilisateurs se reconnectent.

<?php
// Script à lancer une seule fois sur toute la table
$rows = $pdo->query("SELECT id, password_md5 FROM utilisateurs WHERE algo = 'md5'");
$update = $pdo->prepare("UPDATE utilisateurs SET password_hash = ?, password_md5 = NULL, algo = 'legacy' WHERE id = ?");

foreach ($rows as $row) {
    $update->execute([password_hash($row['password_md5'], PASSWORD_DEFAULT), $row['id']]);
}

Étape 2 : convertir au fil des connexions. Pour un compte marqué legacy, on vérifie md5($saisi) avec password_verify(), puis on enregistre une empreinte propre du vrai mot de passe.

<?php
$ok = $utilisateur['algo'] === 'legacy'
    ? password_verify(md5($saisi), $utilisateur['password_hash'])
    : password_verify($saisi, $utilisateur['password_hash']);

if ($ok && $utilisateur['algo'] === 'legacy') {
    $pdo->prepare("UPDATE utilisateurs SET password_hash = ?, algo = 'native' WHERE id = ?")
        ->execute([password_hash($saisi, PASSWORD_DEFAULT), $utilisateur['id']]);
}
🚫
Fausse bonne idée

Garder l’ancienne colonne MD5 « au cas où » après la migration. Elle annule tout le bénéfice : en cas de fuite, l’attaquant l’utiliserait directement. Supprime-la dès que le script de l’étape 1 a tourné.

Les erreurs à éviter avec un mot de passe en PHP

  • Limiter la longueur maximale à 16 ou 20 caractères : laisse tes utilisateurs choisir de longues phrases de passe. Fixe un minimum (12 caractères est un bon repère), pas un maximum trop bas.
  • Appliquer trim() ou htmlspecialchars() sur le mot de passe : tu modifierais ce que l’utilisateur a réellement tapé. Le mot de passe se hache tel quel.
  • Envoyer le mot de passe par e-mail après l’inscription : il voyagerait en clair et resterait dans la boîte de réception.
  • Écrire le mot de passe dans les logs en déboguant un formulaire de connexion.
  • Ne pas limiter les tentatives de connexion : même avec un bon hachage, sans limite, un robot peut tester des milliers de mots de passe sur ton formulaire.
  • Oublier HTTPS : un mot de passe envoyé en HTTP circule en clair sur le réseau, avant même d’atteindre password_hash().
💡
Le bon réflexe

Vérifie la version de PHP de ton serveur avec php -v. En PHP 8.4 ou plus, tu profites du coût bcrypt relevé à 12. Pour installer une version récente sous Windows, suis notre guide installer PHP 8.4 et Composer.

FAQ

Peut-on retrouver un mot de passe à partir de son empreinte password_hash ?

Non. Une empreinte n’est pas un chiffrement : il n’existe pas de clé pour revenir au mot de passe d’origine. La seule attaque possible consiste à tester des mots de passe un par un, et c’est précisément ce que la lenteur de bcrypt ou d’Argon2id rend très coûteux.

Quelle taille doit faire la colonne qui stocke le mot de passe haché ?

Utilise un VARCHAR(255). Une empreinte bcrypt fait 60 caractères, une empreinte Argon2id un peu plus, et la documentation PHP recommande 255 caractères pour anticiper les évolutions de PASSWORD_DEFAULT.

Faut-il ajouter un sel soi-même ?

Non. password_hash() génère automatiquement un sel aléatoire et unique. L’option permettant de fournir son propre sel est dépréciée depuis PHP 7.0 et n’est plus prise en charge depuis PHP 8.0.

Que faire si la base de données a fuité ?

Même avec password_hash, préviens tes utilisateurs, force la réinitialisation des mots de passe et recommande de changer ceux qui étaient réutilisés ailleurs. Un bon hachage ralentit l’attaquant, il ne rend pas les mots de passe faibles invulnérables.

Avant de passer au résumé, un dernier conseil pratique : écris une petite fonction de test qui crée un utilisateur, vérifie la connexion avec le bon mot de passe, puis avec un mauvais. Lance-la à chaque modification de ton système d’authentification. Ce test de quelques lignes t’évitera de casser la connexion de tous tes utilisateurs lors d’une mise à jour, par exemple en changeant d’algorithme ou en modifiant la taille d’une colonne.

Pense aussi à la réinitialisation : le lien envoyé par e-mail doit contenir un jeton aléatoire généré avec random_bytes(), à usage unique, valable peu de temps, et stocké sous forme hachée en base, exactement comme un mot de passe. Une réinitialisation mal conçue est souvent la porte d’entrée la plus simple pour prendre le contrôle d’un compte, même quand chaque mot de passe est parfaitement protégé.

Enfin, documente ces choix dans le README de ton projet : algorithme utilisé, coût choisi et procédure de mise à niveau.

Mot de passe en PHP : le résumé à retenir

  • Ne stocke jamais un mot de passe en clair, chiffré, ou haché avec md5(), sha1() ou sha256.
  • Utilise password_hash($motDePasse, PASSWORD_DEFAULT) et une colonne VARCHAR(255).
  • Vérifie à la connexion avec password_verify(), qui compare à temps constant.
  • Mets à niveau automatiquement les empreintes avec password_needs_rehash().
  • Préfère Argon2id si ton serveur le propose, sinon bcrypt avec le coût par défaut de PHP 8.4.

Protéger chaque mot de passe de tes utilisateurs ne demande que trois fonctions natives. C’est l’un des meilleurs rapports effort/sécurité de tout le développement web.

Conclusion

La sécurité d’un mot de passe ne se joue pas seulement à l’inscription : elle dépend de ce que tu fais de lui pendant toute la vie de ton application. Avec password_hash(), password_verify() et password_needs_rehash(), PHP te donne une solution complète, éprouvée et simple. Mets-la en place dans ton prochain projet, et profites-en pour vérifier tes anciens : si tu as construit une application à partir de notre tutoriel gestion de stock en PHP/MySQL ou de l’application de planning, c’est le moment idéal pour contrôler la façon dont les mots de passe y sont stockés.

Sources

Comments

No comments yet. Why don’t you start the discussion?

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *