Retour au cours

cyber / cybersecurite-fondamentale

OWASP Top 10 : broken authentication et IDOR

Leçon 121 exercice

Explication

Ce que vous allez apprendre

  • Distinguer broken authentication (gestion de session) et IDOR (accès direct à un objet non vérifié)
  • Corriger un IDOR en filtrant systématiquement par propriétaire, pas seulement par identifiant
  • Comprendre pourquoi renvoyer 404 plutôt que 403 évite une fuite d'information par énumération
  • Expliquer pourquoi les UUID réduisent l'énumération sans remplacer le contrôle d'accès
  • Concevoir une gestion de session robuste (régénération d'ID, invalidation après changement de mot de passe)

Dans quel contexte ?

Un utilisateur d'une plateforme de facturation en ligne remarque qu'en changeant simplement l'URL /invoices/1042 en /invoices/1043, il accède à la facture d'un autre client, avec son adresse et le détail de sa commande. L'application vérifiait bien que l'utilisateur était connecté, mais jamais que la facture demandée lui appartenait. C'est un IDOR typique : l'authentification fonctionne, mais l'autorisation sur la ressource précise est absente.

Deux failles très différentes qui se retrouvent souvent ensemble

Cette leçon regroupe deux sujets liés à "qui a le droit d'accéder à quoi". La broken authentication concerne la gestion de l'identité et des sessions dans leur ensemble (des sessions qui n'expirent jamais, des tokens qui restent valides après un changement de mot de passe). L'IDOR (Insecure Direct Object Reference) est plus précis : c'est le cas où une application vous laisse accéder à UNE ressource identifiée par un simple numéro, sans jamais vérifier que cette ressource vous appartient.

L'exemple qui parle à tout le monde

Imaginez un site qui affiche votre facture à l'adresse /invoices/1042. Rien n'empêche, techniquement, de changer l'URL en /invoices/1043 pour voir la facture de quelqu'un d'autre — SAUF si le serveur vérifie explicitement, à chaque requête, que la facture demandée appartient bien à l'utilisateur connecté. L'erreur n'est pas d'utiliser des identifiants numériques simples ; l'erreur est d'oublier cette vérification de propriété.

Pourquoi les UUID ne sont pas une vraie solution

Piège fréquent

Remplacer les identifiants numériques séquentiels par des UUID rend l'énumération ("essayer 1, 2, 3...") beaucoup plus difficile en pratique. Mais si un attaquant obtient un UUID par un autre moyen (un lien partagé, une fuite de logs), l'absence de vérification de propriété reste tout aussi exploitable. C'est une défense en profondeur utile, jamais un remplacement du vrai contrôle d'accès.

Le détail du code 404 vs 403

Retourner "404 non trouvé" plutôt que "403 accès refusé" quand la ressource existe mais n'appartient pas à l'utilisateur peut sembler un détail, mais cela évite à un attaquant de savoir si un identifiant donné existe réellement dans le système — une fuite d'information à part entière.

Défaillance de sessionConséquenceCorrection
Session sans expirationUn token volé reste valide indéfinimentDurée de vie courte + refresh token révocable
ID de session non régénéré au loginSession fixation possibleNouvel ID à chaque élévation de privilège
Pas d'invalidation au changement de mot de passeL'ancien token reste actif après un vol détectéInvalider toutes les sessions de l'utilisateur

Bonne pratique

Une session ou un jeton qui ne peut jamais être invalidé avant sa date d'expiration naturelle pose un problème dès qu'un compte est compromis : prévoyez toujours un mécanisme de révocation, sujet qui sera approfondi dans la leçon sur les attaques JWT.

Commandes & code

OWASP Top 10 : broken authentication et IDOR

Broken authentication regroupe les failles de gestion de session/identité. IDOR (Insecure Direct Object Reference) survient quand une application expose un identifiant interne sans vérifier que l'utilisateur a le droit d'y accéder.

python
# VULNÉRABLE : IDOR classique - aucune vérification que la facture appartient à l'utilisateur connecté
@app.get("/invoices/{invoice_id}")
async def get_invoice_vulnerable(invoice_id: int, current_user=Depends(get_current_user)):
    return db.query(Invoice).filter(Invoice.id == invoice_id).first()
# Un utilisateur connecté peut incrémenter invoice_id (1, 2, 3...) et lire les factures des autres
python
# CORRECT : vérification systématique de la propriété de la ressource (ownership check)
from fastapi import HTTPException

@app.get("/invoices/{invoice_id}")
async def get_invoice_safe(invoice_id: int, current_user=Depends(get_current_user)):
    invoice = db.query(Invoice).filter(
        Invoice.id == invoice_id,
        Invoice.user_id == current_user.id,   # clause obligatoire, pas une vérification a posteriori
    ).first()
    if invoice is None:
        raise HTTPException(status_code=404, detail="Facture introuvable")  # 404, pas 403 (évite l'énumération)
    return invoice
python
# Alternative robuste : identifiants non séquentiels (UUID v4) pour compliquer l'énumération
# NOTE : ce n'est PAS une protection suffisante à elle seule, seulement une défense en profondeur
import uuid
from sqlalchemy import Column
from sqlalchemy.dialects.postgresql import UUID

class Invoice(Base):
    __tablename__ = "invoices"
    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    user_id = Column(UUID(as_uuid=True), nullable=False)
    # ... le contrôle d'accès explicite (filter user_id == current_user.id) reste OBLIGATOIRE
text
# Défaillances classiques de "broken authentication"
1. Identifiants de session prévisibles ou trop courts (facilement devinables/brute-forçables)
2. Absence d'expiration de session -> un token volé reste valide indéfiniment
3. Session non invalidée après changement de mot de passe (l'ancien token reste actif)
4. Session fixation : le serveur accepte un ID de session fourni par l'attaquant avant authentification
5. Timeout d'inactivité absent sur des applications sensibles (banque, admin)
python
# Bonne gestion de session : régénérer l'ID de session à chaque élévation de privilège (login)
# et invalider toutes les sessions existantes lors d'un changement de mot de passe
from datetime import datetime, timedelta, timezone

def create_session(user_id: int, session_store) -> str:
    session_id = secrets.token_urlsafe(32)   # nouvel ID à CHAQUE login, jamais réutilisé (anti session fixation)
    session_store[session_id] = {
        "user_id": user_id,
        "created_at": datetime.now(timezone.utc),
        "expires_at": datetime.now(timezone.utc) + timedelta(hours=8),
    }
    return session_id

def invalidate_all_sessions_for_user(user_id: int, session_store):
    # Appelé lors d'un changement de mot de passe, d'un logout global, ou d'une compromission suspectée
    to_remove = [sid for sid, data in session_store.items() if data["user_id"] == user_id]
    for sid in to_remove:
        del session_store[sid]
http
# JWT : bien gérer l'expiration ET la révocation (un JWT est stateless, donc pas "révocable" nativement)
# -> utiliser une durée de vie courte pour l'access token + refresh token stocké côté serveur (révocable)
POST /auth/refresh
Authorization: Bearer <refresh_token>

# Access token : 15 minutes de durée de vie typique
# Refresh token : stocké en base, révocable, rotation à chaque utilisation (refresh token rotation)
text
# Checklist IDOR / broken auth pour une revue de code
- Chaque endpoint qui prend un ID en paramètre filtre-t-il aussi sur l'utilisateur/tenant courant ?
- Les erreurs "non trouvé" vs "non autorisé" sont-elles uniformisées (404 pour éviter l'énumération) ?
- Les sessions expirent-elles et sont-elles invalidées lors d'un changement de mot de passe ?
- Les tokens JWT ont-ils une durée de vie courte, avec refresh token révocable côté serveur ?

Résumé

  • L'IDOR se corrige en filtrant systématiquement par propriétaire, jamais en se fiant à l'ID seul.
  • Un identifiant non séquentiel (UUID) ralentit l'énumération mais ne remplace pas le contrôle d'accès.
  • La broken authentication couvre session fixation, absence d'expiration, non-révocation après changement de mot de passe.
  • JWT courts + refresh token révocable en base sont le compromis standard stateless/sécurité.

Exercices pratiques

1 disponible
1

Mission : la facture 1042 qui n'est pas la sienne

Objectif : Diagnostiquer un IDOR à partir d'un comportement observé et proposer une correction complète, y compris le code d'erreur renvoyé.

Contexte

Un client de Technologik signale que l'URL https://technologik.local/invoices/1042 affiche sa propre facture, mais qu'en changeant simplement le nombre dans l'URL en 1043, il voit apparaître le nom, l'adresse et le détail de commande d'un autre client, alors qu'il est bien connecté avec son propre compte. Le endpoint concerné vérifie uniquement Depends(get_current_user) pour s'assurer que l'utilisateur est authentifié.

Résoudre l’exercice →