Race conditions (CWE-362) : variantes TOCTOU, dépassement de limite, multi-endpoint et construction partielle — cartographie de surface d'attaque et arbre de décision.
TL;DR
Les race conditions web se divisent en huit catégories définies par la surface d'attaque, le défaut de machine à états, et la technique d'exploitation. Comprendre la catégorie détermine à la fois l'approche de détection et la correction correcte.
TOCTOU (Time-of-Check to Time-of-Use, CWE-367) est le pattern fondamental : une application lit l'état pour prendre une décision (vérification) puis agit sur cette décision (utilisation) sans atomicité. L'attaquant gagne la fenêtre entre vérification et utilisation. Cela apparaît dans les systèmes de fichiers (CVE-2024-50379 compilation JSP Tomcat), les bases de données (vérification de solde puis retrait), et les noyaux (CVE-2024-30088, CVE-2025-22224 — tous deux CISA KEV).
Le dépassement de limite cible tout compteur appliqué : limites d'utilisation sur coupons, cartes cadeaux, appels API, votes, claims de faucet. La vérification du compteur et l'incrément ne sont pas atomiques — les requêtes concurrentes lisent toutes la valeur pré-incrément, passent toutes la vérification, et incrémentent toutes. Couvre CVE-2024-45300 (alf.io codes promo) et CVE-2024-58248 (nopCommerce cartes cadeaux).
La race multi-endpoint exploite des transitions d'état qui s'étendent sur deux endpoints séparés. La machine à états requiert de compléter l'endpoint A avant d'accéder à l'endpoint B, mais la transition n'est pas atomique — racer une requête vers B entre l'écriture de A et le commit de A contourne l'exigence.
La JIT inconsistency (Construction Partielle, Kettle 2023) exploite une fenêtre où un objet existe en DB mais n'est pas encore complètement initialisé. Rails+Devise crée un utilisateur avec confirmation_token=NULL avant qu'un job asynchrone le peuple — un attaquant race une confirmation avec un token vide pendant la fenêtre NULL.
Les races OAuth/JWT ciblent l'application de l'usage unique des codes d'autorisation (HackerOne #55140 — deux paires de tokens depuis un code) et la rotation des tokens de rafraîchissement (rotation concurrente produisant un token spare qui survit à la déconnexion).
| Technique | Max Concurrents | Fenêtre | Quand utiliser |
|---|---|---|---|
| Last-byte sync HTTP/1.1 | 3–5 | 5–20ms | Cibles HTTP/1.1 uniquement |
| Single-packet HTTP/2 (Kettle 2023) | 20–30 | <1ms | Races standard, HTTP/2 requis |
| First sequence sync (Flatt 2024) | ~10 000 | 166ms | Races haute-concurrence (faucet, airdrop) |
HTTP/2 single-packet : retenir le dernier octet de chaque DATA frame HTTP/2 sur toutes les N requêtes, puis relâcher simultanément. L'algorithme Nagle de l'OS coalesce tous les derniers octets dans un paquet TCP, livrant toutes les requêtes au serveur en moins de 1ms. Implémenté dans Turbo Intruder (Engine.BURP2), Burp Repeater "Send group in parallel", et h2spacex.
First sequence sync : exploite la fragmentation IP pour retarder le premier fragment IP (numéro de séquence TCP 0) d'une connexion HTTP/2 pendant que tous les fragments suivants procèdent. Le serveur attend le fragment retardé avant de traiter toute requête dans la fenêtre de connexion. Nécessite un accès raw packet via Scapy (extension h2spacex).
La détection des race conditions combine plusieurs techniques complémentaires :
Identification structurelle : identifier les patterns d'URL transactionnels — /redeem, /transfer, /withdraw, /vote, /claim, /oauth/token, /reset, /confirm. Ce sont des candidats indépendamment du comportement de réponse.
Burst avec différentiel de réponse :
async def burst_probe(url: str, headers: dict, body: dict, n: int = 20):
async with httpx.AsyncClient(http2=True, verify=False) as client:
await client.get(url.rsplit("/", 1)[0] + "/", headers=headers) # Réchauffement
tasks = [client.post(url, headers=headers, json=body) for _ in range(n)]
responses = await asyncio.gather(*tasks, return_exceptions=True)
statuses = [r.status_code for r in responses if not isinstance(r, Exception)]
successes = statuses.count(200)
return {"n": n, "successes": successes, "signal_race": successes > 1}Preuve read-race-read : GET état autoritatif avant burst, exécuter burst, GET état après. Si le compteur s'est incrémenté de N au lieu de 1, ou N resource IDs distincts ont été créés, la race est confirmée. Cela élimine les faux positifs des réponses mises en cache, de la logique de retry, et de l'idempotence qui retourne des 200 mis en cache.
| Catégorie | Surface | Outils | Correction |
|---|---|---|---|
| TOCTOU (DB) | Fintech, e-commerce, auth | Turbo Intruder, httpx | SELECT FOR UPDATE, UPDATE atomique |
| TOCTOU (filesystem) | Handlers d'upload, systèmes de build | PoC d'upload concurrent | Temp dir hors webroot |
| TOCTOU (noyau) | OS, hyperviseurs | Exploit local | Patch fournisseur (CISA KEV) |
| Dépassement de limite | Coupon, vote, faucet, quota | Turbo Intruder | INSERT ON CONFLICT DO NOTHING |
| Multi-endpoint | Flux auth, checkout | Burp Repeater group | Audit de machine à états |
| JIT inconsistency | Rails+Devise, création utilisateur | HTTP/2 concurrent | Initialisation synchrone |
| OAuth/JWT | Fournisseurs auth | asyncio.gather | Invalidation atomique du code |
| Race cache CDN | Next.js ISR, edge functions | GET concurrent | Verrouillage de revalidation |
La race cible-t-elle une condition de garde sur une ressource EXISTANTE et ENTIÈREMENT INITIALISÉE ?
├── OUI → TOCTOU (CWE-367)
│ └── La garde est-elle sur un chemin de fichier ? → TOCTOU filesystem
│ └── La garde est-elle sur une colonne DB ? → TOCTOU base de données
│
├── NON → Cible-t-elle une fenêtre pendant la CRÉATION/INITIALISATION d'un OBJET ?
│ └── OUI → JIT Inconsistency (Kettle 2023)
│ └── Fenêtre de token NULL ? (pattern Devise)
│ └── Fenêtre de permission par défaut ? (admin avant rétrogradation)
│
└── NON → Exploite-t-elle DEUX ENDPOINTS SÉPARÉS ?
└── OUI → Race multi-endpoint (CWE-841)
└── Pattern bypass auth ? (signup + confirm + login)
└── Race de permission ? (grant + action avant revoke)| CVE | Catégorie | CVSS | CISA KEV | Impact |
|---|---|---|---|---|
| CVE-2024-30088 | TOCTOU noyau | 7.0 | OUI | LPE SYSTEM Windows |
| CVE-2025-22224 | TOCTOU hyperviseur | 9.3 | OUI | VM escape VMware ESXi |
| CVE-2025-38352 | TOCTOU noyau | 7.4 | OUI | LPE noyau Linux |
| CVE-2024-50379 | TOCTOU filesystem | 9.8 | NON | RCE Apache Tomcat |
| CVE-2024-58248 | Dépassement de limite | 7.5 | NON | Vidange carte cadeau nopCommerce |
| CVE-2024-45300 | Dépassement de limite | 7.5 | NON | Bypass code promo alf.io |
| CVE-2025-32421 | Race cache CDN | 5.4 | NON | Cache poisoning Next.js |
| HackerOne #55140 | Race OAuth | N/A | NON | Persistance famille tokens (2,5K$) |
Trois entrées CISA KEV confirmées en 15 mois (juin 2024–septembre 2025) confirment que les race conditions — en particulier les TOCTOU au niveau noyau et hyperviseur — sont exploitées dans les semaines suivant la divulgation.
TOCTOU (base de données) : UPDATE conditionnel atomique (UPDATE WHERE condition RETURNING id) ou SELECT FOR UPDATE dans une transaction. Jamais SELECT séparé puis UPDATE.
Dépassement de limite : INSERT ON CONFLICT DO NOTHING + incrément conditionnel du compteur seulement sur insert réussi. Ou UPDATE WHERE count < max RETURNING id atomique.
Transaction dupliquée : header Idempotency-Key (Redis SETNX, TTL 24h) + contrainte UNIQUE DB sur l'ID d'opération comme fallback.
Upload de fichier : upload vers répertoire temp non-web-accessible, valider complètement, puis déplacer atomiquement vers le webroot. Jamais écrire dans le webroot d'abord.
OAuth/JWT : invalidation atomique du code — UPDATE codes SET used=true WHERE id=$1 AND used=false RETURNING access_token. Si 0 lignes, le code était déjà consommé.
JIT inconsistency : initialisation synchrone — compléter toutes les étapes de setup avant que l'objet devienne visible externement. Jamais créer une ligne en DB incomplètement initialisée et accessible à d'autres requêtes.
La taxonomie de Kettle 2023 identifie huit catégories : (1) TOCTOU classique — fenêtre check-and-use sur filesystem ou DB ; (2) Dépassement de limite — coupon, carte cadeau, vote, faucet via compteur non atomique ; (3) Race multi-endpoint — transition d'état sur deux endpoints ; (4) Collision single-endpoint — write-write race sur état de session partagé ; (5) JIT inconsistency — fenêtre de construction partielle ; (6) Race OAuth/JWT — double-rédemption de code, rotation de token ; (7) HTTP/2-spécifique — attaque single-packet, first sequence sync ; (8) Infrastructure — race de revalidation cache CDN, verrou distribué.
Le TOCTOU (Time-of-Check Time-of-Use) race une vérification explicite contre une action explicite sur une ressource existante : vérification solde >= montant vs action de retrait. La JIT inconsistency race contre la fenêtre d'initialisation d'un objet — l'objet existe en DB mais son état est incomplet. Exemple : Rails+Devise crée une ligne utilisateur avec confirmation_token=NULL avant qu'un job asynchrone peuple le token. L'attaquant exploite la fenêtre NULL, pas une garde sur un objet déjà complet.
'Smashing the State Machine' de James Kettle (Black Hat USA 2023) a démontré l'attaque single-packet : 20-30 requêtes HTTP/2 dans un paquet TCP via synchronisation du dernier octet, éliminant le jitter réseau et rendant les races distantes aussi fiables que locales. 'Beyond the Limit' de GMO Flatt Security (CODE BLUE 2024) a étendu cela avec le first sequence sync — jusqu'à 10 000 requêtes concurrentes en 166ms via fragmentation IP.
Rails + Devise (fenêtre de construction partielle / token NULL, bien documenté par Kettle). Spring Boot + Hibernate (isolation READ COMMITTED par défaut — pattern SELECT puis UPDATE courant). Express + Mongoose (findOne + update au lieu de findOneAndUpdate atomique). Django sans expressions F() (obj.field += 1; obj.save()). Applications PHP utilisant JWT (verrouillage natif de session non applicable). Next.js 14.x-15.1.x (race de revalidation ISR CVE-2025-32421).
Rechercher les endpoints qui (1) appliquent une contrainte usage unique ou de limite (redeem, claim, apply, withdraw, vote), (2) effectuent une transition d'état (confirm, activate, verify, reset), (3) créent des ressources uniques avec contraintes d'unicité (register, signup, issue-token), ou (4) impliquent des opérations financières (transfer, payout, purchase). Tout endpoint qui lit l'état pour prendre une décision puis mute cet état dans une séquence non atomique est un candidat. Le signal structurel — pas la réponse — est ce qui compte : un 409 Conflict sur des requêtes séquentielles avec un 200 sur des requêtes concurrentes est la confirmation classique.
Le read-race-read (RRR) est la technique de confirmation qui élimine les faux positifs des réponses 200 mises en cache ou de la logique de retry. Étape 1 : GET l'état autoritatif (solde, compteur, statut token) et enregistrer la valeur. Étape 2 : exécuter le burst N-concurrent. Étape 3 : GET l'état autoritatif à nouveau. Si l'état a changé de plus d'une unité — solde baissé de N×montant au lieu de 1×montant, compteur incrémenté de N au lieu de 1, N resource IDs distincts créés — la race est confirmée indépendamment des codes de réponse. Le RRR est requis pour tout finding de race au-dessus de la sévérité Informational.
L'isolation SERIALIZABLE prévient toutes les race conditions au niveau base de données en détectant les conflits write-write et read-write entre transactions concurrentes et en abandonnant l'une d'elles. READ COMMITTED (la valeur par défaut PostgreSQL) ne prévient pas le TOCTOU — chaque instruction voit un instantané cohérent mais deux instructions dans la même transaction peuvent voir des états différents si un write concurrent commite entre elles. En pratique : utiliser SERIALIZABLE pour les opérations financières critiques, ou les patterns plus ciblés SELECT FOR UPDATE / UPDATE conditionnel atomique qui atteignent la sécurité sans overhead de sérialisation complète.
Le dépassement de limite standard race un incrément de compteur — le compteur est la garde. Les races OAuth/JWT ciblent l'invalidation de token à usage unique : le code d'autorisation, le token de réinitialisation, ou le refresh token doit être marqué consommé exactement une fois. La race exploite la fenêtre entre la vérification de validité (SELECT valid=true) et l'invalidation (UPDATE used=true ou DELETE). La conséquence est qualitativement différente : le dépassement de limite produit une consommation de ressource supplémentaire ; les races OAuth produisent un accès non autorisé persistant — l'attaquant conserve une famille de tokens valide après que la session de la victime est révoquée.
Le rate limiting gateway et les règles WAF ne préviennent pas les race conditions au niveau applicatif. Les rate limiters opèrent sur des comptages de requêtes par fenêtre temporelle, pas sur l'atomicité des transitions d'état de l'application. L'attaque single-packet livre toutes les N requêtes en moins de 1ms — plus vite qu'un compteur token-bucket ne peut s'incrémenter entre la requête 1 et la requête 2. Les règles WAF ne peuvent pas inspecter si un SELECT et un UPDATE sont atomiques. Les corrections au niveau applicatif (SQL atomique, clés d'idempotence, SELECT FOR UPDATE) sont les seules contre-mesures efficaces.