Contournement des quotas d'achat par utilisateur, des rate limits et des restrictions promo un-par-compte via parameter tampering, race conditions ou application UI uniquement.
TL;DR
Field(ge=1, le=N) côté serveur + verrou de ligne SELECT ... FOR UPDATE + contrainte UNIQUE en base comme dernier filet de sécurité.Le contournement de limite est une classe de faille de logique métier où un attaquant contourne un quota défini par l'application — « max 2 par client », « une promo par compte », « 5 invitations gratuites » — en exploitant un écart entre la vérification qui applique la limite et l'écriture qui valide le changement d'état. Classifié sous CWE-840 (Business Logic Errors) et exposé par OWASP WSTG-BUSL-04 (Test for Limit Validation), il se trouve dans la catégorie OWASP Top 10 2021 A04:2021 — Insecure Design, parce qu'aucune sanitisation d'entrée ni authentification ne corrigera une garantie transactionnelle absente dans le workflow lui-même.
Le score CVSS v3.1 de base est 6,5 (Medium) avec le vecteur CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N. Bien qu'aucune CVE canonique n'ait été publiée pour le contournement de limite (la classe est typiquement rapportée via programmes privés de bug bounty plutôt que via divulgations CVE), les CVE proches CVE-2020-11007 (Shopizer quantité négative) et CVE-2023-45854 (Shopkit integer overflow) démontrent des primitives équivalentes de contournement de borne de quantité dans des stacks e-commerce en production.
La vulnérabilité est fréquemment confondue avec le rate limiting (CWE-770), mais les modèles de menace sont différents. Le rate limiting protège l'infrastructure des floods de requêtes (brute force, DoS, scraping) en limitant le débit des requêtes par IP ou par token dans le temps. Le contournement de limite protège les ressources métier — crédits, redemptions, unités achetées, payouts de parrainage — en appliquant des règles liées à l'identité de l'utilisateur et à la sémantique produit. Un contournement de rate limit permet à un attaquant d'envoyer 1 000 requêtes OTP par seconde ; un contournement de limite métier permet au même attaquant d'acheter 1 000 unités d'un article promotionnel à 0,01 $. Les deux peuvent coexister sur le même endpoint, mais ils sont diagnostiqués et corrigés indépendamment.
La raison pour laquelle le contournement de limite est si courant dans les stacks modernes est architecturale. Les single-page applications appliquent les règles métier en JavaScript (boutons désactivés, champs cachés, compteurs côté client), tandis que l'API REST ou GraphQL sous-jacente manque souvent de la garde équivalente côté serveur. Les frontières microservices rendent le check-and-set atomique difficile, et le multiplexage HTTP/2 permet à un attaquant de placer 50 requêtes dans des fenêtres sub-millisecondes — transformant l'écart en exploit fiable et reproductible.
Les applications imposent des limites au comportement utilisateur pour protéger le revenu, prévenir les abus et assurer une allocation équitable des ressources. Ces limites tombent dans cinq catégories, chacune avec un emplacement d'application typique et un mode de défaillance typique :
| Type de limite | Exemple | Emplacement d'application courant |
|---|---|---|
| Quantité d'achat | Article promotionnel « max 2 par client » | Validation de formulaire frontend uniquement |
| Quotas d'usage | Tier gratuit : 100 appels API par jour | Compteur côté serveur dans Redis ou DB |
| Rate limits | 5 tentatives OTP par compte | Compteur middleware sur IP ou session |
| Plafonds parrainage / promo | « Parrainer 5 amis max » par campagne | Vérification de ligne DB avant insertion |
| Promos un-par-compte | « Réduction première commande, usage unique » | Flag booléen dans le record utilisateur |
Le pattern de faille critique est le même dans les cinq cas : la vérification de limite se produit dans une couche (UI, middleware ou code applicatif), mais l'opération finale qui change l'état se produit dans une couche différente sans re-vérification. La fenêtre entre la vérification et le commit est la surface d'exploit, et elle peut être élargie de microsecondes à secondes en exploitant le parameter tampering sur la valeur limite elle-même.
VULNERABLE PATTERN:
1. GET /api/limit-check -> returns {"allowed": true} <- check
2. POST /api/redeem-promo <- commit (no re-check)
SECURE PATTERN:
1. POST /api/redeem-promo -> BEGIN TRANSACTION
SELECT count(*) FROM redemptions WHERE user_id = ? FOR UPDATE
IF count >= max: ROLLBACK, return 429
INSERT redemption; COMMITLe pattern sécurisé fusionne vérification et commit en une seule transaction de base de données atomique avec un verrou au niveau ligne. Toute requête concurrente doit attendre que la première transaction commit ou rollback avant que son propre SELECT FOR UPDATE puisse procéder, éliminant entièrement la fenêtre TOCTOU. Le pattern vulnérable, en revanche, permet à N requêtes concurrentes d'observer indépendamment count = 0 avant qu'aucune d'elles n'incrémente le compteur — et chaque INSERT suivant réussit parce qu'aucune contrainte d'unicité ne l'interdit.
Six techniques d'attaque couvrent la grande majorité des découvertes de contournement de limite dans les rapports de bug bounty en production :
| Variante | Technique | Impact |
|---|---|---|
| Quantité négative | Soumettre qty=-1 pour soustraire du total panier | Crédit compte / articles gratuits au checkout |
| Quantité fractionnaire | Soumettre qty=0.6 pour facturation à prix partiel | Réduction sur chaque commande (pattern Zomato) |
| Integer overflow | Soumettre qty=999999 pour faire déborder l'arithmétique | Total proche de zéro ou négatif via overflow |
| Race condition | HTTP/2 single-packet, 20–50 requêtes parallèles | Plusieurs redemptions d'une promo à usage unique |
| Contournement UI uniquement | Rejouer une action réussie via Burp Repeater | Le backend manque de la garde que l'UI applique |
| Parameter forcing | Manipuler un champ d'état (status=approved, tier=premium) | Raccourci workflow contournant la vérification |
Les quantités négatives et fractionnaires exploitent l'absence de validation de signe et de type sur les entrées numériques. Le serveur accepte la valeur, exécute qty * unit_price, et soit crédite l'utilisateur (résultat négatif), soit facture un total réduit (résultat fractionnaire). L'integer overflow vise les back-ends à langage typé où l'arithmétique non vérifiée sur int32 déborde : 2147483648 * 1 devient -2147483648.
La classe la plus omniprésente est l'application UI uniquement. Les SPA modernes désactivent le bouton « Apply Coupon » après une utilisation, cachent le formulaire « Add Retailer » après la troisième entrée, ou grisent le bouton « Submit » jusqu'à ce qu'un cooldown s'écoule. Aucune de ces applications ne franchit la frontière réseau. Un seul rejeu Burp Repeater du POST initialement réussi — sans aucune UI impliquée — contourne toute la vérification.
Les race conditions exploitent l'écart entre la lecture du compteur au niveau applicatif et l'écriture en base. En utilisant la technique HTTP/2 single-packet attack de PortSwigger (synchronisation last-byte, 2023), 20–50 requêtes identiques arrivent au serveur dans des fenêtres sub-millisecondes. Chaque requête passe indépendamment la vérification count < max avant qu'aucune d'elles n'écrive la nouvelle ligne de redemption, et toutes réussissent.
Zomato — HackerOne #403783 (250 $, Medium) : Un chercheur a modifié le champ support_rider_amount en une valeur décimale négative. Le serveur a accepté la valeur et a réduit le total de la commande proportionnellement, obtenant effectivement une réduction sur chaque commande passée via l'application. La couche de validation vérifiait que le champ était numérique mais n'a jamais imposé une contrainte de non-négativité ni comparé le champ à un maximum de règle métier. Le correctif a nécessité un validateur positif uniquement sur chaque champ d'entrée monétaire.
Upserve / OLO — HackerOne #364843 (Medium, bounty non divulguée) : En incluant un article de menu avec quantity: -1 dans le payload du panier, le prix total de la commande était réduit. La quantité négative se propageait sans contestation à travers le pipeline de facturation parce que le serveur validait que le SKU de l'article existait mais ne validait jamais le signe de la quantité. L'exploit fonctionnait sur tous les articles du menu, pas seulement les promotionnels, ce qui en faisait une faille de checkout à l'échelle de la catégorie plutôt qu'un bug sur un seul article.
Curve — HackerOne #672487 (High, bounty non divulguée) : Les comptes Curve non premium avaient une limite stricte de 3 retailers éligibles à la fonctionnalité de cashback « Curve Cash ». Après avoir atteint 3, l'UI désactivait le bouton « Add retailer ». Un chercheur a capturé la requête POST /api/retailers/add initiale dans Burp, l'a rejouée via Repeater, et a ajouté avec succès un 4e, 5e et Nème retailer — chacun gagnant un cashback qui aurait dû être restreint au tier premium. Le backend n'avait aucune vérification de compteur côté serveur équivalente ; toute l'application était JavaScript uniquement.
HackerOne #115007 (race condition, limite d'invitations) : Une plateforme appliquait un plafond de 3 invitations par compte en utilisant une lecture de compteur au niveau applicatif avant l'insertion, sans verrou au niveau ligne. En tirant 7 POST d'invitation concurrents via Burp Intruder, un chercheur a envoyé avec succès 7 invitations — chacune observant indépendamment count < 3 avant que les autres n'écrivent leurs lignes. Le correctif a nécessité un SELECT ... FOR UPDATE sur le compteur d'invitations, plus une contrainte d'unicité sur (user_id, invite_token).
UPchieve — HackerOne #1296597 (100 $, Medium) : Une erreur de logique métier dans l'application de la limite de session permettait aux étudiants de consommer plus de sessions de tutorat que ne le permettait leur tier de compte. La vérification était effectuée au moment de la requête de session mais n'était pas re-vérifiée au moment du démarrage de session, et une API de création de session lente permettait à un attaquant de mettre en file plusieurs requêtes de session avant qu'aucune d'elles ne se termine.
Starbucks — divulgation Sakurity (2015, Critical, 500 $) : Une race condition dans le flux de transfert de valeur de carte cadeau permettait des transferts en double. Deux requêtes de transfert concurrentes depuis la même carte source passaient toutes deux la vérification « solde suffisant ? » avant qu'aucune ne déduise le solde, doublant effectivement la valeur transférée. Rapporté via responsible disclosure ; patché en quelques jours. Le cas est largement cité parce qu'il démontre que même les chemins de code de qualité paiement échouent à utiliser des transactions atomiques quand la vérification et l'écriture sont séparées entre services.
La méthodologie de test de contournement de limite de PortSwigger et OWASP WSTG-BUSL-04 suit trois phases : énumération des paramètres numériques, parameter tampering et test de concurrence.
Étape 1 — Cartographier tous les paramètres numériques. Parcourir l'application avec le proxy Burp activé, en se concentrant sur les endpoints checkout, promo/coupon, parrainage et abonnement. Cataloguer chaque champ numérique : quantity, amount, count, limit, uses, seats, credits, tier, discount. Noter le type, la plage attendue, et si l'UI applique la borne.
Étape 2 — Manipuler systématiquement avec Burp Repeater. Pour chaque paramètre numérique, envoyer qty=0, qty=-1, qty=-9999, qty=0.1, qty=9999999, qty="1", qty="1abc", qty=null. Surveiller les réponses 200 avec corps de succès, le total qui change en faveur de l'attaquant, l'absence de 400/422 retourné, ou les erreurs référençant un underflow/overflow arithmétique.
Étape 3 — Lancer des tests de race condition avec Turbo Intruder ou Burp Repeater Group. Capturer une requête bornée par limite et l'envoyer à Repeater. Utiliser la fonctionnalité Repeater Group de Burp (2022+) avec « Send group in parallel (single-packet attack) » activé — cela délivre 20–50 requêtes identiques dans une seule trame TCP en utilisant le multiplexage HTTP/2. Si 2 requêtes ou plus retournent un succès là où une seule devrait, le TOCTOU est confirmé. Vérifier via GET /api/account ensuite.
Les scanners web génériques manquent généralement le contournement de limite parce que la vulnérabilité requiert un contexte applicatif : quels endpoints ont des limites métier, quels paramètres représentent des ressources métier, et quelles post-conditions vérifier. Un scanner qui tire qty=-1 contre chaque endpoint génère du bruit sans découvertes confirmées — il n'a aucun moyen de vérifier l'effet de bord sans s'authentifier, naviguer vers l'endpoint de vérification et parser du JSON spécifique à l'application.
Signaux automatisés efficaces : HTTP 200 retourné sur un POST en double vers un endpoint à usage unique, corps de réponse contenant des totaux monétaires négatifs ou nuls, discount_applied qui s'incrémente sur des requêtes identiques, et divergence entre les réponses API directes et les réponses du flux UI. Chacun nécessite une baseline et un oracle de comparaison.
BreachVex détecte ceci par des techniques complémentaires. L'une établit une baseline par endpoint avec une requête légitime, puis tire des requêtes concurrentes avec un timing favorable au TOCTOU et compare les corps de réponse. Une autre parcourt les catalogues de formulaires et les codes découverts lors de la cartographie de la surface d'attaque, identifie les endpoints à sémantique d'usage unique (promo, invite, signup-bonus), et les rejoue avec capture de preuve pour confirmer que la limite peut être dépassée.
La vérification (« l'utilisateur a-t-il utilisé cette promo ? ») et l'écriture (« marquer la promo comme utilisée ») doivent être une seule opération de base de données atomique. Le pattern en deux étapes — SELECT puis INSERT hors d'une transaction — est la cause racine de quasiment chaque contournement de limite par race condition TOCTOU.
Python vulnérable (FastAPI + SQLAlchemy) :
async def redeem_promo(user_id: str, promo_code: str, db: AsyncSession):
# Check
existing = await db.execute(
select(PromoRedemption)
.where(PromoRedemption.user_id == user_id)
.where(PromoRedemption.promo_code == promo_code)
)
if existing.scalar_one_or_none():
raise HTTPException(409, "Promo already redeemed")
# Commit (race window opens here — concurrent requests all pass the check)
db.add(PromoRedemption(user_id=user_id, promo_code=promo_code))
await db.commit()Python sécurisé — transaction atomique avec SELECT FOR UPDATE :
async def redeem_promo(user_id: str, promo_code: str, db: AsyncSession):
async with db.begin():
# SELECT FOR UPDATE locks the row; concurrent transactions wait
result = await db.execute(
select(PromoRedemption)
.where(PromoRedemption.user_id == user_id)
.where(PromoRedemption.promo_code == promo_code)
.with_for_update()
)
if result.scalar_one_or_none():
raise HTTPException(409, "Promo already redeemed")
# Atomic insert; unique constraint catches any race survivor
db.add(PromoRedemption(user_id=user_id, promo_code=promo_code))
# COMMIT happens here; any duplicate raises IntegrityError -> 409Contrainte de base de données comme dernier filet de sécurité (toujours ajouter) :
CREATE UNIQUE INDEX idx_promo_redemptions_user_promo
ON promo_redemptions (user_id, promo_code);La contrainte d'unicité est la dernière ligne de défense. Même si le code applicatif est contourné, la base refuse l'insertion en double au niveau du stockage avec une IntegrityError, que l'application attrape et retourne en 409 Conflict. C'est de la défense en profondeur : verrou au niveau applicatif pour le flux normal, contrainte au niveau base pour les survivants de race et les bugs de chemin de code.
Ne jamais faire confiance aux limites côté JavaScript comme seule application. Désactiver un bouton, cacher un formulaire, ou compter les clics dans localStorage est une fonctionnalité UX, pas un contrôle de sécurité. La méthodologie de test OWASP WSTG-BUSL-04 teste explicitement la « validation côté client uniquement » comme mode de défaillance. Toute limite métier doit être ré-appliquée côté serveur, dans la même transaction que l'écriture qui change l'état.
Chaque entrée numérique franchissant la frontière de l'API doit être validée pour le signe, la plage et le type avant que toute logique métier ne s'exécute. Cela arrête toute la famille du parameter tampering (négatif, fractionnaire, overflow) à la lisière de l'application.
Node.js (Zod + Express) :
import { z } from "zod";
import type { Request, Response } from "express";
const AddToCartSchema = z.object({
product_id: z.string().uuid(),
// Reject negatives, fractionals, overflows, and string coercion
quantity: z
.number()
.int({ message: "Quantity must be a whole number" })
.min(1, { message: "Quantity must be at least 1" })
.max(100, { message: "Quantity cannot exceed 100 per order" }),
});
export async function addToCart(req: Request, res: Response) {
const parsed = AddToCartSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(422).json({ errors: parsed.error.format() });
}
const { product_id, quantity } = parsed.data;
// Safe: quantity is guaranteed integer in [1, 100]
await cartService.addItem(req.user.id, product_id, quantity);
return res.status(200).json({ ok: true });
}La chaîne z.number().int().min(1).max(100) rejette chaque variante de parameter tampering en une seule déclaration : int() bloque 0.6 (Zomato), min(1) bloque -1 (Upserve) et 0, max(100) bloque 999999 (overflow). Combiné au pattern atomique SELECT FOR UPDATE et à une contrainte d'unicité en base, l'application a trois couches de défense indépendantes — toutes les trois doivent échouer simultanément pour qu'un exploit réussisse.
Pour les opérations de paiement et de promo, exiger également une clé d'idempotence dans chaque requête de mutation. Le serveur stocke (user_id, idempotency_key) -> result dans Redis avec un TTL de 24 heures et retourne le résultat caché pour les doublons. Cela neutralise les attaques de replay au niveau du protocole. La conception de la clé d'idempotence de Stripe est la référence canonique.
qty=-1 pour bypasser le minimum).Le contournement de limite est une classe de faille de logique métier où un attaquant contourne un quota défini par l'application — « max 2 par client », « une promo par compte », « 5 invitations gratuites » — en exploitant un écart entre la vérification de la limite et le commit de l'écriture d'état. Mappé à CWE-840 (Business Logic Errors), exposé par OWASP WSTG-BUSL-04, et listé sous OWASP A04:2021 (renommé A06:2025 — Insecure Design). Cas réels : HackerOne #403783 Zomato, HackerOne #672487 Curve, HackerOne #364843 OLO.
Six techniques. (1) Quantité négative (qty=-1) inverse l'arithmétique de consommation. (2) Quantité fractionnaire (qty=0.6) sous-consomme le quota. (3) Integer overflow (qty=999999) fait déborder le compteur. (4) HTTP/2 single-packet attack lance N requêtes parallèles avant la vérification de limite. (5) Contournement de l'application UI uniquement via Burp Repeater (HackerOne #672487 Curve). (6) Parameter forcing sur les champs d'état (used_count=0). Les bounties réelles vont de 100 $ (UPchieve) à 25 K $ et plus pour les contournements impactant le revenu.
Le rate limiting (CWE-770 Allocation of Resources Without Limits) est un contrôle d'infrastructure — limiter les requêtes par seconde par IP/utilisateur pour prévenir le DoS. Le contournement de limite (CWE-840 Business Logic Errors) est la défaite d'un quota métier défini par l'application — « 3 essais gratuits par e-mail », « 5 invitations par campagne », « plafond de retrait quotidien de 500 $ ». Le rate limiting est appliqué au niveau du middleware (Cloudflare, NGINX, SlowAPI). Les limites métier doivent être appliquées au niveau applicatif avec des transactions atomiques (SELECT FOR UPDATE), car elles concernent un état spécifique au domaine, pas seulement des compteurs de requêtes.
Parce que l'UI n'est qu'un client parmi d'autres de l'API — désactiver le bouton « Ajouter » après 3 entrées n'empêche pas une requête Burp Repeater de frapper POST /api/retailers/add une quatrième fois. HackerOne #672487 (Curve, sévérité High) est le cas d'école : un compte non premium avait une limite de 3 retailers appliquée par l'UI ; le chercheur a rejoué le même POST via Burp et a ajouté un 4e, 5e et Nème retailer parce que le backend ne vérifiait jamais le compteur. Toute limite doit être appliquée côté serveur avec une vérification-écriture atomique.
Défense à trois couches. (1) Frontend : schema Zod ou Pydantic avec .max(1). (2) Transaction applicative : BEGIN; SELECT count(*) FROM redemptions WHERE user_id=? AND promo_id=? FOR UPDATE; if count >= 1 rollback; INSERT redemption; COMMIT. (3) Contrainte base de données : UNIQUE INDEX redemptions_user_promo_idx ON (user_id, promo_id) pour que les insertions en doublon échouent au niveau du schéma même si la logique applicative est contournée. La combinaison défait le parameter tampering, les race conditions, et la manipulation directe de la base.
OWASP WSTG-BUSL-04 — Test for the Circumvention of Limit Validations est la méthodologie OWASP Web Security Testing Guide pour le contournement de quotas métier. Elle demande aux testeurs d'identifier chaque limite quantitative dans l'application (plafonds d'achat, rate limits, redemption counts, taille de fichier, plafonds de retrait) et de tester chacune avec : valeurs nulles, négatives, fractionnaires, integer overflow, requêtes parallèles, contournement UI, et parameter forcing. WSTG-BUSL-04 fait partie de la famille BUSL plus large couvrant les 10 cas de test OWASP de logique métier.
Trois signaux. (1) La réponse backend montre count > maximum affiché par l'UI (par ex. 5 retailers attachés alors que l'UI dit max 3). (2) Rejouer le même POST après que le bouton UI est désactivé — un 200 OK avec un nouvel ID de ressource confirme que le backend n'applique aucune limite. (3) Test de soumission concurrente : envoyer 10 requêtes POST parallèles via Turbo Intruder ; si toutes les 10 réussissent, aucune application atomique n'existe. BreachVex automatise les trois signaux — comparaison baseline-vs-concurrent et rejeu des codes à usage unique — sans orchestration Burp manuelle.