Manipulation de prix (CWE-840) : modifier des champs de formulaire cachés, corps JSON ou totaux panier pour acheter à des prix arbitraires.
TL;DR
price fourni par le client sans recherche catalogue.item.price par db.products.get(item.product_id).price.La manipulation de prix est une vulnérabilité de logique métier dans laquelle une application accepte une valeur monétaire — price, unit_price, amount, total ou subtotal — directement depuis une requête client et l'utilise comme coût de référence d'une transaction. Comme la valeur est fournie par le client, un attaquant peut intercepter la requête dans un proxy, changer price=49900 en price=1 et finaliser l'achat à un centime. Le serveur ne re-valide jamais la valeur contre son propre catalogue produit. Cette vulnérabilité est cataloguée comme CWE-840 (Business Logic Errors) avec une faiblesse contributive CWE-20 (Improper Input Validation), et relève de OWASP A04:2021 — Insecure Design.
La vulnérabilité est invisible aux scanners d'injection traditionnels. Aucun payload ne provoque d'erreur 500. Aucune regex ne matche une chaîne malveillante. La requête est syntaxiquement et sémantiquement valide — elle contient simplement un nombre que le développeur n'a jamais voulu rendre modifiable. La détection nécessite de raisonner sur l'intention de chaque champ, raison pour laquelle les outils DAST et les WAF passent tous deux à côté. Les pentesters manuels et les scanners conscients de la logique métier la détectent en interceptant le flux de checkout et en modifiant les champs numériques un à un.
L'exposition financière est proportionnelle au volume de commandes. Une seule faille non détectée sur une plateforme e-commerce à fort trafic peut produire des pertes à six chiffres avant que la réconciliation ne révèle l'écart. C'est pourquoi le top des bounties HackerOne pour la logique métier place régulièrement la manipulation de prix dans les cinq sous-types les mieux récompensés, et pourquoi les divulgations en contexte Shopify ont historiquement rapporté 25 000 à 150 000 dollars selon que la faille touche un seul magasin ou le checkout au niveau plateforme.
L'exploitation repose sur une seule frontière de confiance brisée : le serveur délègue l'autorité de tarification au client. Le flux est identique à travers les stacks à formulaire caché, REST et GraphQL.
L'attaque se résume à quatre étapes déterministes :
unit_price, sans interroger son propre catalogue produit.La requête vulnérable canonique, capturée contre une stack e-commerce réelle :
POST /api/cart/add HTTP/2
Host: shop.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGc...
Cookie: session=abc123
{
"product_id": "jacket-heavyweight-001",
"quantity": 1,
"price": 1
}Un backend vulnérable répond {"cart_id": "cart_xyz", "line_total": 1, "currency": "USD"} et facture un centime pour une veste à 499 $. L'en-tête Authorization est valide — l'attaquant est un client authentifié — ce qui explique précisément pourquoi les scanners qui notent les requêtes sur la sémantique HTTP ne voient rien d'anormal.
Le champ price est souvent ajouté pendant des fonctionnalités de tarification dynamique (bundles, remises membres, géo-tarification) et jamais retiré quand la fonctionnalité est annulée. Auditez chaque champ monétaire dans vos schémas panier et commande — s'il est modifiable, il est exploitable.
Cinq techniques distinctes produisent le même résultat ; les contrôles défensifs doivent couvrir les cinq.
| Variante | Technique | Impact |
|---|---|---|
| Édition champ caché | Réécriture DevTools de <input type="hidden" name="price"> avant soumission | Critique — achat à 0,01 $ |
| Interception Burp (REST/GraphQL) | Modifier price dans le corps JSON ou l'argument GraphQL unitPrice | Critique — achat au prix attaquant |
| Prix/quantité négatif | Mettre price=-500 ou quantity=-1 pour un total panier net négatif | Critique — crédit magasin émis |
| Integer overflow | Soumettre quantity=9223372036854775807 (INT64_MAX), wrap vers total négatif | Élevé — bypass des contrôles >= 0 |
| Override total panier | POST {"cart_total": 0.01} à un endpoint checkout qui saute la re-sommation | Critique — commande entière à 0,01 $ |
| Confusion de devise | Payer en INR pendant que le serveur traite le nombre brut comme USD | Élevé — réduction de prix x80 |
| Replay de payload signé | Rejouer un ancien payload panier signé après augmentation du prix catalogue | Moyen — borné par le delta de l'ancien prix |
La variante quantité négative mérite une attention particulière. De nombreux backends vérifient price >= 0 mais ne vérifient jamais quantity >= 1, donc un attaquant garde un article à 1 000 $ avec une quantité -1 pendant qu'un autre article à bas coût porte la commande, et l'arithmétique produit un total négatif que le processeur de paiement interprète comme une obligation de remboursement. L'integer overflow est l'inverse : des valeurs positives qui mathématiquement basculent négatives, déjouant la validation naïve.
Les divulgations suivantes établissent que la manipulation de prix est un problème actuel, rémunéré, multi-plateformes — pas une préoccupation théorique.
HackerOne #218748 — Falsification de paramètres sur checkout XML e-commerce — Un attaquant a intercepté le payload XML de checkout, modifié l'élément <price> et finalisé des achats aux prix de son choix. Sévérité : Élevée. Bounty payé (montant non divulgué). Le rapport démontre que les stacks XML sont également exposées ; la classe de vulnérabilité est agnostique au transport.
HackerOne #364843 — Manipulation du prix total OLO via quantité négative — Sur la plateforme de commande de repas OLO, mettre quantity=-1 pour un article à forte valeur tout en gardant d'autres articles dans le panier amenait l'arithmétique backend à soustraire le coût de cet article du total de commande. Les attaquants pouvaient commander des repas gratuitement ou recevoir un crédit net. Le correctif a nécessité de valider quantity >= 1 à la frontière API, pas seulement à l'UI.
HackerOne #927661 — Plateforme de paiement, montant falsifiable par fraction négative — Un processeur de paiement acceptait un paramètre amount en fraction négative, réduisant le total dû. Bounty : 500 $ (Moyen, plafonné car l'exploitabilité était bornée à de petites réductions). Cela illustre que la sévérité varie directement avec l'ampleur de l'écart possible par rapport au prix légitime.
HackerOne #1562515 — Glovo integer overflow — Après que Glovo a remédié à un premier rapport de falsification de prix en supprimant le champ price, un suivi a démontré que le champ quantity restant était vulnérable à l'integer overflow : mettre quantity à 9223372036854775807 (INT64_MAX) faisait basculer le total ligne en négatif. La leçon est que retirer un champ est insuffisant quand l'arithmétique dépend encore de valeurs client.
HackerOne #1446090 — Krisp, downgrade de sièges sans revalidation — Un endpoint PUT /subscription acceptait un nombre de seats inférieur au nombre déjà provisionné sans re-valider contre le palier de facturation. Les clients pouvaient downgrader leur facturation tout en conservant l'accès à tous les sièges provisionnés — une manipulation de prix exprimée via l'état d'abonnement plutôt que via un panier.
CVE-2025-56426 — Manipulation de prix panier sur Bagisto CMS — Bagisto, plateforme e-commerce open-source basée sur Laravel, acceptait un paramètre price dans son API d'ajout au panier sans recherche catalogue côté serveur. Noté CVSS 8.1 Élevé avec le vecteur CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H. Patché dans Bagisto 2.2.3 en avril 2025. Le code pré-patch est un exemple d'école : le contrôleur passait request()->price directement au constructeur de la ligne panier.
PortSwigger Web Security Academy publie trois labs de référence qui reproduisent ces patterns : Excessive trust in client-side controls (modifier price pour la veste en cuir), Low-level logic flaw (integer overflow 32 bits sur les totaux ligne) et Infinite money logic flaw (discount stacking de carte cadeau). Les labs sont utiles pour former les pentesters car l'exploitation est identique aux cas en production ci-dessus.
Burp Suite (ou Caido) est l'outil standard. La méthodologie est procédurale et reproductible :
POST /cart/add intercepté (ou équivalent), chercher dans le corps de la requête tout champ numérique dont la valeur correspond au prix affiché — noms courants : price, unit_price, amount, cost, total, subtotal, unitPrice.document.querySelectorAll('input[type=hidden]') depuis la console navigateur capture les patterns legacy.1, puis 0.01, puis -9999, en envoyant chaque variante via Burp Repeater. Toujours tester une valeur qui diffère exactement de 1 (4989 vs 4990) — les changements subtils passent la validation de format tout en prouvant le problème de confiance.confirmed — changement d'état financier vérifié.Répéter la procédure pour l'endpoint checkout lui-même. Certaines applications valident correctement les prix sur /cart/add mais acceptent un cart_total fourni par le client sur /checkout/confirm, sautant la re-sommation des line items. Les deux surfaces doivent être testées indépendamment.
Les scanners DAST génériques (ZAP, Burp Active Scan, Acunetix) passent à côté de cette classe car aucun payload ne déclenche d'erreur. La détection spécialisée en logique métier nécessite :
^(unit_)?price|amount|cost|total|subtotal$ suivies d'une mutation automatisée et d'un diff de réponse.Limites : les scanners qui n'ont pas de gestion de session authentifiée ne peuvent pas atteindre l'endpoint panier. Les scanners qui n'ont pas de crawl stateful ne peuvent pas corréler le prix affiché avec le champ de corps de requête. Des faux positifs apparaissent sur des endpoints légitimes de tarification dynamique (remises bundle, tarification contractuelle B2B) où price est un input valide d'un appelant privilégié.
BreachVex détecte cela via son moteur de logique métier, qui crawle les flux panier et checkout avec une session authentifiée, identifie les champs numériques dont les valeurs correspondent aux prix catalogue, et asserte l'intégrité en soumettant des variantes falsifiées et en vérifiant que le total de commande suit l'input modifié plutôt que la valeur catalogue.
Le correctif canonique tient en une règle : ne jamais lire le prix depuis la requête. Toujours le rechercher par product_id depuis le catalogue de référence. Ci-dessous une comparaison côte à côte en FastAPI ; l'équivalent Express est identique en forme.
Python vulnérable (FastAPI) :
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
router = APIRouter()
class CartItem(BaseModel):
product_id: str
quantity: int
price: float # BUG: writeable from client
@router.post("/cart/add")
async def add_to_cart(item: CartItem, db=Depends(get_db)):
cart_entry = CartEntry(
product_id=item.product_id,
quantity=item.quantity,
unit_price=item.price, # <-- attacker-controlled
)
db.add(cart_entry)
db.commit()
return {"line_total": item.quantity * item.price}Python corrigé (FastAPI) :
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
router = APIRouter()
class CartAddRequest(BaseModel):
product_id: str
quantity: int = Field(ge=1, le=100)
# price field deliberately omitted
@router.post("/cart/add")
async def add_to_cart(item: CartAddRequest, db=Depends(get_db)):
product = db.query(Product).filter(Product.id == item.product_id).first()
if not product:
raise HTTPException(status_code=404, detail="Product not found")
cart_entry = CartEntry(
product_id=item.product_id,
quantity=item.quantity,
unit_price=product.price, # <-- authoritative catalog price
)
db.add(cart_entry)
db.commit()
return {"line_total": item.quantity * product.price}Deux détails structurels comptent au-delà de la recherche catalogue. D'abord, CartAddRequest ne déclare pas de champ price — Pydantic rejettera les champs supplémentaires si model_config = ConfigDict(extra='forbid') est défini, éliminant les surprises de mass-assignment des sérialiseurs ORM. Ensuite, quantity est borné par Field(ge=1, le=100) pour bloquer les valeurs négatives et les tentatives d'integer overflow à la frontière API, avant toute arithmétique.
Une seconde couche de contrôle protège contre les failles de logique ailleurs dans le pipeline panier. Même si chaque requête add était correctement tarifée, recalculer le total de commande au checkout depuis le catalogue — ne jamais faire confiance aux unit prices stockés de la table panier et ne jamais accepter un cart_total fourni par le client.
@router.post("/checkout/confirm")
async def confirm_order(order_id: str, db=Depends(get_db)):
cart_items = db.query(CartEntry).filter(
CartEntry.order_id == order_id
).all()
calculated_total = sum(
db.query(Product).get(item.product_id).price * item.quantity
for item in cart_items
)
# Always recompute; never trust submitted totals
charge_payment(amount=calculated_total)Pour les architectures stateless où le panier vit côté client ou dans un cache CDN edge, générer un HMAC côté serveur du contenu du panier au moment où les prix sont résolus, et re-vérifier la signature au checkout :
import hmac, hashlib, json, os
SECRET = os.environ["CART_SIGNING_KEY"]
def sign_cart(cart: dict) -> str:
payload = json.dumps(cart, sort_keys=True)
return hmac.new(
SECRET.encode(), payload.encode(), hashlib.sha256
).hexdigest()
def verify_cart(cart: dict, signature: str) -> bool:
return hmac.compare_digest(sign_cart(cart), signature)Toute modification des quantités, prix ou IDs de produits invalide la signature, et l'endpoint checkout refuse la commande. La clé de signature doit tourner indépendamment des secrets de session et résider dans un gestionnaire de secrets — jamais dans le contrôle de version.
Stocker les valeurs monétaires en centimes entiers, jamais en float. L'arithmétique flottante introduit une dérive de précision que les attaquants exploitent (0.1 + 0.2 != 0.3) et les vulnérabilités d'integer overflow comme le wrap INT64_MAX de Glovo dépendent de l'arithmétique entière signée — choisir des types entiers non signés ou contraindre les plages explicitement.
qty=-1 ou integer overflow.La manipulation de prix est une faille de logique métier dans laquelle une application accepte une valeur monétaire (price, unit_price, amount, total) directement depuis une requête client et l'utilise comme coût de référence. Les attaquants interceptent la requête dans Burp Repeater et changent price=49900 en price=1, finalisant l'achat à un centime. Mappée à CWE-840 (Business Logic Errors) avec CWE-20 contributif (Improper Input Validation), et OWASP A04:2021 (renommé A06:2025 — Insecure Design). CVE-2025-56426 (manipulation de prix panier sur Bagisto v2.3.6) est l'exemple canonique de 2025.
Trois méthodes principales. (1) Éditer les champs cachés de formulaire via les DevTools du navigateur avant soumission (pattern legacy). (2) Intercepter POST /api/cart/add dans Burp Proxy et modifier le champ price dans le corps JSON. (3) Manipuler cart_total ou subtotal sur /checkout/confirm si le serveur saute la re-sommation des line items. Cas réels : HackerOne #364843 (OLO/Upserve, quantité négative réduisant le total), HackerOne #1562515 (Glovo, integer overflow sur le prix de commande), HackerOne #218748 (falsification de paramètres Adobe). Tous exploitent la confiance serveur dans les valeurs client.
La manipulation de prix modifie directement un total fourni par le client (mettre price à zéro, envoyer quantity -1) — corrigée en recalculant les totaux côté serveur depuis un catalogue de confiance. Le discount stacking envoie le bon code coupon avec la bonne identité utilisateur mais le réutilise plus de fois que prévu via des race conditions TOCTOU sur la table coupon — corrigé par des transactions atomiques et des clés d'idempotence. Les deux coexistent souvent (appliquer une remise puis manipuler le prix), mais les contrôles sont distincts : la recomputation côté serveur ne protège pas contre les race conditions, et le verrouillage ne protège pas contre la substitution de valeur client.
Défense à cinq couches. (1) Recherche catalogue côté serveur : ne jamais accepter price du client ; toujours SELECT price FROM products WHERE id=? sur chaque action panier. (2) Pydantic avec extra='forbid' ou zod .strict() pour rejeter les champs inattendus. (3) Bornes de quantité : conint(ge=1, le=999) pour les articles, condecimal(gt=0) pour les montants. (4) Tokens panier signés HMAC pour que le contenu du panier ne puisse pas être édité entre affichage et checkout. (5) Utiliser des centimes entiers (DECIMAL(10,2) ou BIGINT) — jamais de floats IEEE 754 pour l'argent. Stripe et Shopify imposent les cinq.
Les scanners DAST génériques (ZAP, Burp Scanner, Nuclei, Acunetix) ne peuvent pas détecter de manière fiable la manipulation de prix car il n'y a pas de payload malveillant à matcher — la requête ressemble à un achat normal, juste avec un nombre différent. La détection nécessite de comprendre ce que devrait être un prix, ce qui implique de corréler la requête avec un catalogue produit et des fourchettes de prix connues. Les outils spécialisés en logique métier (Pynt, Escape BLST, StackHawk BLT lancé en décembre 2025) et la détection de logique métier de BreachVex tentent cette approche via comparaison à une baseline par produit et détection hors plage.
CVE-2025-56426 (Bagisto CMS v2.3.6, CVSS 6.5, Laravel/PHP) est le cas phare 2025 pour la manipulation de prix panier dans une stack e-commerce open-source majeure. Les CVE de manipulation de prix WordPress passent typiquement par les extensions WooCommerce et sont suivis par Patchstack plutôt que NVD. Les cas côté Shopify passent par le programme HackerOne privé avec des bounties entre 25 000 et 150 000 dollars pour un bypass complet du total checkout. CVE-2020-11007 (Shopizer Java Spring) couvre la variante quantité négative — étroitement liée mais techniquement un CWE différent.
La manipulation de prix est un sous-type spécifique de falsification de paramètres appliqué aux champs monétaires. La falsification de paramètres est la classe générale — modifier tout paramètre contrôlé par le client (user_id pour IDOR, role=admin pour escalade de privilèges, price=1 pour des commandes gratuites). La manipulation de prix se concentre sur le sous-ensemble à impact financier où le paramètre modifié détermine directement le coût de la transaction. Le WSTG OWASP couvre les deux sous WSTG-BUSL-05 et WSTG-INPV-04 respectivement. La défense pour la manipulation de prix est la recherche catalogue côté serveur ; la défense pour la falsification générale de paramètres est l'autorisation et la validation côté serveur.