cyber / cybersecurite-fondamentale
OWASP Top 10 : broken authentication et IDOR
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 session | Conséquence | Correction |
|---|---|---|
| Session sans expiration | Un token volé reste valide indéfiniment | Durée de vie courte + refresh token révocable |
| ID de session non régénéré au login | Session fixation possible | Nouvel ID à chaque élévation de privilège |
| Pas d'invalidation au changement de mot de passe | L'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.
# 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# 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# 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# 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)# 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]# 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)# 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
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é.