Authentification6 min de lecture

Énumération de comptes utilisateur : mécanismes, signaux et parades

Comprendre comment les formulaires de connexion, d'inscription et de réinitialisation divulguent l'existence de comptes, et comment aligner sécurité et expérience utilisateur sans fausses promesses.

L'énumération de comptes utilisateur est une faiblesse applicative permettant à un tiers non authentifié de déterminer si un identifiant donné (adresse email, nom d'utilisateur, numéro de téléphone) est associé à un compte actif au sein du système.

Cette vulnérabilité n'est pas une brèche directe de données ni une prise de contrôle immédiate. Elle constitue en revanche une phase de reconnaissance préparatoire redoutable : elle permet d'affiner des attaques par force brute ciblée, du bourrage d'identifiants (credential stuffing), ou des campagnes de harcèlement et d'ingénierie sociale en confirmant la présence d'une personne sur un service sensible.

Dans les architectures modernes (Next.js, Remix, API REST/GraphQL), cette divulgation découle le plus souvent d'une volonté bienveillante mais mal calibrée d'assister l'utilisateur lors de la saisie de ses formulaires.


Les trois surfaces d'exposition majeures

L'énumération s'infiltre principalement au travers de trois flux d'authentification standard.

flowchart TD
    Attaquant([Attaquant]) -->|Sonde avec email| F1[Formulaire de Connexion]
    Attaquant -->|Sonde avec email| F2[Formulaire d'Inscription]
    Attaquant -->|Sonde avec email| F3[Réinitialisation de mot de passe]
    
    F1 -->|Erreur différenciée ou delta de timing| Divulgation[Confirmation de l'existence du compte]
    F2 -->|Refus immédiat : Email déjà utilisé| Divulgation
    F3 -->|Message : Aucun compte associé| Divulgation
    
    Divulgation --> Exploitation[Attaque par dictionnaire / Bourrage d'identifiants]

1. Le formulaire de connexion (Login)

L'erreur classique consiste à distinguer l'absence du compte de l'invalidité du mot de passe :

  • Mauvaise pratique : renvoyer "Cet utilisateur n'existe pas" dans un cas, et "Mot de passe incorrect" dans l'autre.
  • Conséquence : l'attaquant valide son dictionnaire d'adresses en observant simplement le message de rejet.

2. Le formulaire de réinitialisation de mot de passe (Password Reset)

Il s'agit de la surface la plus couramment vulnérable :

  • Mauvaise pratique : afficher "Un lien de réinitialisation vous a été envoyé" pour un utilisateur existant, mais renvoyer une erreur 404 ou le message "Cette adresse email n'est associée à aucun compte actif" dans le cas contraire.
  • Conséquence : le formulaire devient un oracle de vérification d'adresses consultable à l'infini.

3. Le formulaire d'inscription (Signup)

Sur l'inscription, la prévention est techniquement plus subtile. Lorsqu'un utilisateur tente de s'inscrire avec une adresse déjà présente :

  • Mauvaise pratique : refuser violemment la soumission avec "Un compte existe déjà avec cette adresse. Veuillez vous connecter." sans aucun contrôle de débit ni friction.
  • Risque : pour des services confidentiels, de santé ou spécifiques, la confirmation d'enregistrement d'une personne constitue déjà une violation de sa vie privée.

Signaux de divulgation : HTTP et Timing Attacks

L'énumération ne se limite pas au libellé textuel des messages d'erreur. Les applications modernes trahissent fréquemment l'état de leur base par des signaux annexes.

Codes de statut HTTP et charge utile

Une API qui répond :

  • 404 Not Found pour un identifiant inconnu,
  • 401 Unauthorized pour un identifiant existant avec un mauvais mot de passe, fournit une confirmation binaire automatisable sans même analyser le texte JSON.

Attaques temporelles (Timing Attacks)

Même lorsque le message renvoyé est rigoureusement identique ("Identifiants invalides"), le temps de réponse du serveur peut être révélateur :

  • Si l'identifiant n'existe pas : l'application interrompt la requête en 15 millisecondes.
  • Si l'identifiant existe : l'application calcule le hachage cryptographique du mot de passe fourni (ex. bcrypt avec coût 12, ou Argon2id), ce qui mobilise 250 à 350 millisecondes de CPU.

L'attaquant observe la distribution statistique des temps de réponse : un delta mesurable suffit à automatiser le tri des comptes valides.

// EXEMPLE DE TIMING ATTACK INVOLONTAIRE
export async function POST(req: Request) {
  const { email, password } = await req.json();
  const user = await db.user.findUnique({ where: { email } });

  if (!user) {
    // Sortie immédiate : temps d'exécution ~15ms
    return NextResponse.json({ error: "Identifiants invalides" }, { status: 401 });
  }

  // Calcul cryptographique lourd : temps d'exécution ~300ms
  const isValid = await verifyPassword(password, user.passwordHash);
  if (!isValid) {
    return NextResponse.json({ error: "Identifiants invalides" }, { status: 401 });
  }

  return NextResponse.json({ success: true });
}

Arbitrage UX vs Sécurité : adopter une posture pragmatique

L'éradication totale des signaux d'énumération engendre parfois une friction d'usage notable :

  • Si l'inscription ne signale pas qu'un compte existe déjà, l'utilisateur légitime qui a oublié son inscription ne comprend pas pourquoi il ne reçoit pas le message d'activation habituel.
  • Sur la réinitialisation de mot de passe, ne jamais indiquer si l'email a été trouvé peut dérouter l'utilisateur qui a fait une faute de frappe dans son adresse.

La réponse standard recommandée

  1. Sur la connexion : aucun compromis. Le message doit être strictement neutre et générique : "Identifiant ou mot de passe incorrect".
  2. Sur la réinitialisation : réponse uniforme. Afficher systématiquement : "Si un compte correspond à cette adresse, vous recevrez un lien de réinitialisation d'ici quelques instants." Si le compte existe, l'email part avec le jeton. Si le compte n'existe pas, aucun email ne part (ou un email informatif invitant à créer un compte est expédié à l'adresse renseignée pour court-circuiter l'oracle sur l'écran).
  3. Sur l'inscription : si le modèle d'affaires exige d'avertir l'utilisateur d'un doublon, cette route doit impérativement être soumise à une limitation stricte du débit (rate limiting) et à une validation par défi (ex. Turnstile) pour empêcher le scraping de masse.

Protections complémentaires obligatoires

La neutralité des messages ne constitue que la première couche de défense. Pour neutraliser l'énumération à grande échelle, des mesures actives sont indispensables :

  1. Limitation de débit par IP et par cible (Rate Limiting) :
    • Bloquer ou ralentir les soumissions répétées depuis la même IP.
    • Limiter également les tentatives ciblant le même identifiant, pour contrer les botnets distribués.
  2. Atténuation du timing par opération fictive :
    • En cas d'utilisateur inexistant, exécuter un hachage factice avec un sel constant pour égaliser artificiellement la durée de traitement avant de répondre.
  3. Défis automatisés invisibles :
    • Déployer un défi cryptographique silencieux (Cloudflare Turnstile, reCAPTCHA v3) dès la deuxième tentative infructueuse.
  4. Surveillance des pics d'échecs 401/404 :
    • Alerter les équipes en cas d'anomalie de volume sur les routes d'authentification.

Ce que GVO observe lors de l'audit

L'analyse de code GVO inspecte statiquement les routes d'API, les Server Actions et les gestionnaires d'authentification pour relever les divulgations manifestes :

  • GVO-AUTH-LOGIN-ENUMERATION-V1 : Détecte les messages d'erreur asymétriques au sein d'une même route de login (ex. retour explicite signalant un compte introuvable).
  • GVO-AUTH-SIGNUP-ENUMERATION-V1 : Détecte les retours directs exposant l'existence d'une adresse email sur les routes d'inscription sans présence de garde-fous ou de limitation de débit.
  • GVO-AUTH-RESET-ENUMERATION-V1 : Détecte les flux de réinitialisation de mot de passe renvoyant une réponse d'erreur conditionnée à l'existence du compte en base de données.
// PATTERN CORRIGÉ POUR LA RÉINITIALISATION (GVO-AUTH-RESET-ENUMERATION-V1)
export async function handlePasswordReset(email: string) {
  const user = await db.user.findUnique({ where: { email } });

  if (user) {
    const token = await generateResetToken(user.id);
    await sendResetEmail(user.email, token);
  } else {
    // Optionnel : simuler un délai réaliste ou consigner l'événement sans altérer le retour client
    await simulateEqualLatency();
  }

  // Réponse uniforme et neutre dans tous les cas
  return {
    success: true,
    message: "Si un compte est associé à cette adresse, un lien de réinitialisation vous a été transmis."
  };
}

Sources et références

  • OWASP Top 10 API Security Risks : API3:2023 - Broken Object Property Level Authorization & User Enumeration.
  • OWASP Authentication Cheat Sheet : Recommandations officielles sur les messages d'erreur neutres et la prévention des oracles d'authentification.
  • CWE-204 : Observable Response Discrepancy (Discrépance observable dans la réponse).
  • NIST SP 800-63B : Digital Identity Guidelines - Authentication and Lifecycle Management.

Vérifier ce point sur votre projet

GVO confronte le code et les configurations de votre application aux règles de sécurité réelles.