Objectifs
À la fin de ce cours, tu seras capable de :
- Identifier les hypothèses implicites d'un parcours métier (prix, quantités, étapes, rôles) que le serveur ne vérifie pas.
- Analyser un flux de commande ou de paiement pour repérer valeurs négatives, prix fournis par le client, coupons réutilisables et étapes contournables.
- Démontrer une race condition de type limit-overrun avec tes propres comptes, de petits montants et un volume de requêtes limité.
- Évaluer l'impact financier d'une faille logique sans causer de préjudice réel.
- Rédiger une recommandation de correction précise : recalcul serveur, transactions atomiques, contraintes d'unicité, clés d'idempotence, allow-list de champs, machine à états.
Pourquoi c'est important en bug bounty
Les failles de logique métier ne ressemblent à aucune signature : pas de caractère spécial, pas d'erreur SQL, pas de payload. La requête est parfaitement valide, c'est l'enchaînement ou la valeur qui viole une règle que le développeur croyait garantie. Les scanners automatiques ne les trouvent presque jamais, ce qui en fait un terrain où la réflexion humaine paie, avec beaucoup moins de doublons que sur les classes de vulnérabilités techniques.
Les sévérités vont de Low (un coupon appliqué deux fois sur un panier de test) à Critical (modifier son rôle par mass assignment, vider un solde de portefeuille en parallèle). Les programmes e-commerce, fintech et SaaS à abonnement y sont particulièrement sensibles, car l'impact se chiffre directement en euros.
Ce qui distingue un rapport payé d'un rapport fermé :
- Payé : la règle métier violée est énoncée clairement (« un coupon ne peut servir qu'une fois »), la démonstration est reproductible sur des comptes de test, l'impact est chiffré et le chercheur a annulé les commandes sans rien encaisser.
- Fermé : « j'ai mis une quantité négative et j'ai eu une erreur 500 » sans conséquence métier, un comportement documenté et voulu (cumul de promotions autorisé), ou pire, un chercheur qui a réellement obtenu des marchandises ou de l'argent.
Le mécanisme
Toute faille de logique repose sur un écart entre l'invariant attendu et ce que le serveur impose réellement. Un invariant est une règle qui doit rester vraie quoi qu'il arrive : « le total payé est égal à la somme des prix catalogue », « un coupon a au plus une utilisation », « on ne peut pas expédier une commande non payée ». Les failles surviennent pour trois grandes raisons.
1. La confiance accordée au client. Le serveur reprend une valeur que le navigateur ou l'application mobile lui envoie (prix, total, remise, rôle, statut) au lieu de la recalculer ou de la relire en base. Tout ce qui transite par le client est modifiable dans un proxy.
2. Le contrôle dispersé dans le parcours. Les vérifications sont faites à une étape (page panier) mais pas à celle qui compte (validation de commande). Si l'on peut appeler directement l'étape finale, les contrôles intermédiaires sont contournés.
3. La concurrence. Le code vérifie une condition, puis agit en fonction d'elle, en deux opérations séparées. C'est le schéma TOCTOU (Time Of Check to Time Of Use) : entre la vérification et l'écriture s'ouvre une fenêtre de course. Si plusieurs requêtes arrivent dans cette fenêtre, chacune voit l'ancien état et toutes passent le contrôle. Lorsque la règle violée est une limite (une utilisation, un solde, un stock), on parle de limit-overrun.
Voici le code typique qui produit ce diagramme :
def appliquer_coupon(panier, code):
coupon = db.query("SELECT id, used FROM coupons WHERE code = %s", (code,))
if coupon is None or coupon.used: # 1. CHECK
raise ErreurMetier("Coupon invalide")
panier.remise += 10 # fenêtre de course
db.execute("UPDATE coupons SET used = true WHERE id = %s", (coupon.id,)) # 2. USERien n'est faux ligne par ligne. C'est l'absence d'atomicité entre la lecture et l'écriture qui crée la faille.
Le mass assignment (API3:2023)
Cas particulier de confiance excessive : le framework lie automatiquement tous les champs JSON reçus aux propriétés de l'objet métier. Si le modèle User possède un champ role ou is_verified, l'ajouter dans la requête de mise à jour du profil suffit à le modifier. L'OWASP API Security Top 10 2023 le range dans API3:2023 Broken Object Property Level Authorization, avec l'exposition excessive de propriétés.
Question de réflexion : pourquoi un contrôle JavaScript qui empêche de saisir une quantité négative ne protège-t-il de rien ?
Parce qu'il s'exécute sur la machine de l'utilisateur. Un proxy d'interception permet de modifier la requête après ce contrôle, et un client d'API peut l'envoyer sans jamais charger le JavaScript. Les contrôles côté client améliorent l'ergonomie ; seuls les contrôles côté serveur garantissent un invariant.
Où chercher
Concentre-toi sur les fonctionnalités où une règle métier a une valeur : argent, quotas, droits, unicité.
- Panier et paiement : quantités, prix unitaires, frais de port, total, devise, champs cachés renvoyés au serveur.
- Coupons, cartes cadeaux, parrainage, points de fidélité : réutilisation, cumul, application après validation, transfert entre comptes.
- Parcours en plusieurs étapes : inscription avec vérification, checkout, KYC, réinitialisation de mot de passe, onboarding avec essai gratuit.
- Limites et quotas : nombre de votes, d'invitations, de retraits, d'essais de code, de places réservées.
- Mise à jour de profil et d'objets via API : tout
PUT/PATCHqui accepte un objet JSON complet. - Clients multiples : application mobile, ancienne version d'API (
/api/v1/), application partenaire. Les règles y sont souvent implémentées différemment du site web.
Signaux faibles dans le proxy :
- des champs
price,amount,total,discount,role,statusdans les requêtes du client ; - des identifiants d'étape (
step=3) ou des jetons d'étape réutilisables ; - des réponses d'API qui renvoient plus de propriétés que le formulaire n'en affiche (indice de mass assignment) ;
- une opération sensible qui n'exige ni verrou visible ni clé d'idempotence (
Idempotency-Keyabsent).
Méthode de test
Règles du jeu : tes comptes de test, de petits montants, moyens de paiement de test si le programme en fournit, annulation systématique des commandes, rien d'encaissé, rien d'expédié. Pour les tests de concurrence, garde un volume minimal (quelques requêtes) et respecte les limites de débit indiquées par le programme.
Étape 1 — Formuler les invariants. Avant de toucher une requête, écris les règles que le parcours devrait garantir : « total = somme des prix catalogue × quantités », « coupon BIENVENUE10 utilisable une fois par compte », « pas de commande sans paiement validé ».
Étape 2 — Tester les valeurs limites. Rejoue la requête d'ajout au panier en faisant varier une seule valeur à la fois : quantité 0, -1, 0.5, très grande valeur, prix modifié.
POST /api/cart/items HTTP/1.1
Host: shop.techshop.test
Cookie: session=<compte-test-A>
Content-Type: application/json
{"productId": 4012, "quantity": 1, "unitPrice": 0.01}Si le panier affiche ensuite un article à 0,01 €, le serveur fait confiance au prix envoyé. Combine ensuite avec une quantité négative sur un autre article : si le total baisse, le serveur accepte des montants négatifs.
Étape 3 — Tester les coupons. Applique deux fois le même code, deux codes différents non cumulables, un code après l'étape de validation, le même code depuis deux comptes de test. Vérifie à chaque fois le total recalculé par le serveur, pas l'affichage.
Étape 4 — Sauter des étapes. Enregistre le parcours complet, puis rejoue directement la requête finale (confirmation de commande) sans passer par l'étape de paiement, ou réutilise un jeton d'étape d'une ancienne commande. Observe si la commande passe au statut « payée ».
Étape 5 — Comparer les clients. Effectue la même action depuis le web et l'application mobile (ou l'ancienne version d'API). Une règle présente côté web mais absente côté mobile est une faille à part entière.
Étape 6 — Tester le mass assignment. Récupère l'objet complet via un GET, puis renvoie un PATCH contenant une propriété supplémentaire non exposée par le formulaire :
PATCH /api/v2/users/me HTTP/1.1
Host: api.acme.test
Authorization: Bearer <jeton-compte-test-A>
Content-Type: application/json
{"displayName": "Testeur A", "plan": "enterprise"}Relis l'objet : si plan a changé, la propriété est assignable. Remets aussitôt la valeur d'origine.
Étape 7 — Tester la concurrence (conceptuellement, avec retenue). Pour une opération limitée (coupon unique, retrait, vote), prépare quelques requêtes identiques et envoie-les simultanément. Les outils modernes regroupent plusieurs requêtes HTTP/2 pour que leurs derniers octets partent dans un même paquet : c'est la single-packet attack décrite par PortSwigger, qui réduit la gigue réseau et rend la fenêtre de course observable. Dans Burp Suite, cela correspond à l'envoi d'un groupe d'onglets « en parallèle ». Deux à cinq requêtes suffisent pour une preuve ; inutile d'en envoyer des centaines.
Question de réflexion : tu envoies cinq requêtes parallèles d'application d'un coupon unique et deux remises sont appliquées. Que dois-tu faire avant de rédiger le rapport ?
Reproduire une à deux fois pour écarter un effet de bord, noter le nombre de succès obtenus sur le nombre de requêtes envoyées, puis annuler la commande ou retirer les remises et ne rien valider. Le rapport chiffrera l'impact potentiel (remise multipliée par le nombre de requêtes gagnantes) sans qu'aucun préjudice réel n'ait eu lieu.
Variantes et défenses courantes
Variantes fréquentes.
- Valeurs négatives ou décimales : quantité
-1qui produit un crédit, quantité0.5arrondie différemment par le calcul du prix et par le décompte du stock. - Débordement et arrondi : très grandes quantités dépassant la capacité d'un entier, arrondis répétés sur des conversions de devise.
- Prix ou remise fournis par le client : champ caché, paramètre JSON, ou panier sérialisé côté client.
- Coupons cumulables ou réutilisables : contrôle limité au cumul « dans la même requête », réutilisation après annulation, coupon lié à l'e-mail mais pas au compte.
- Saut d'étapes et machine à états implicite : transition directe de « panier » à « confirmée ».
- Incohérences multi-clients : règles différentes entre web, mobile et API historique.
- Race conditions : double utilisation d'un code unique, double retrait sur un solde, dépassement d'un quota d'invitations, création de deux comptes avec le même identifiant unique.
Pourquoi les défenses partielles échouent.
- Contrôles côté client : contournés par le proxy.
- Vérification au mauvais endroit : le total est validé à l'affichage du panier, puis recalculé à partir des valeurs envoyées lors de la confirmation.
- Rejet des valeurs négatives uniquement : les décimales, les zéros et les très grandes valeurs passent.
- Limitation de débit contre les races : un rate limit de 10 requêtes par seconde laisse passer cinq requêtes simultanées. La concurrence n'est pas un problème de volume mais d'atomicité.
- Vérification applicative avant l'écriture (« si déjà utilisé, refuser ») : c'est précisément le schéma TOCTOU vulnérable.
- Liste de blocage de champs contre le mass assignment : chaque nouveau champ sensible ajouté au modèle devient assignable par défaut.
Prouver l'impact sans nuire
Ce qui est accepté :
- des captures de requêtes et de réponses montrant le total recalculé par le serveur (par exemple un panier à 0,01 € ou une double remise) ;
- la page de récapitulatif avant paiement, ou une commande créée puis annulée immédiatement ;
- pour une race condition : le nombre de requêtes envoyées, le nombre de succès, et l'état final de la ressource ;
- pour le mass assignment : l'objet relu après modification, puis restauré.
Ce qui est interdit :
- finaliser un paiement réel à prix manipulé, recevoir une marchandise ou un service, retirer de l'argent, transférer des points vers un compte réel ;
- toucher aux comptes, soldes ou coupons d'autres utilisateurs ;
- inonder l'application de requêtes parallèles ou épuiser un stock réel ;
- conserver un avantage obtenu (plan premium, crédit) après la démonstration.
Preuve minimale : l'invariant violé en une phrase, la ou les requêtes modifiées, la réponse prouvant l'effet, le montant concerné, et la mention explicite « commande annulée, aucun montant encaissé, valeur restaurée ». Si une finalisation semble nécessaire pour prouver l'impact, demande l'autorisation du programme au préalable : certains fournissent des moyens de paiement de test ou acceptent un remboursement encadré.
Évaluer la sévérité
| Scénario | Vecteur CVSS 3.1 | Score |
|---|---|---|
| Mass assignment permettant à un utilisateur de s'attribuer le rôle administrateur | AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N | 8.1 Élevée |
| Prix unitaire envoyé par le client et accepté lors de la confirmation | AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N | 6.5 Moyenne |
| Race condition permettant de dépenser deux fois le solde d'une carte cadeau | AV:N/AC:H/PR:L/UI:N/S:U/C:N/I:H/A:N | 5.3 Moyenne |
| Race condition appliquant deux fois un coupon de bienvenue de 10 € | AV:N/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:N | 3.1 Faible |
Le CVSS mesure mal l'impact financier : beaucoup de programmes ajustent la sévérité en fonction du montant en jeu et de la facilité d'industrialisation. Les races conditions reçoivent AC:H car leur succès dépend d'un timing que l'attaquant ne maîtrise pas entièrement. Argumente toujours l'impact métier en plus du score : « un attaquant peut obtenir n'importe quel article du catalogue pour 0,01 € » parle davantage au triage qu'un vecteur seul.