Client-Supplied Price Mints a Live Payment Token for Any Amount
The Risk
An attacker could decide for themselves what a bulk course-license order would cost, then have the company's own website hand the card processor a live payment page set to that amount. In one test a genuine order worth around $64,500 produced a payment page that charged only $100, and the same trick would work for almost any order down to a small fixed minimum near $63. No account and no login were needed, and no victim had to do anything - just two ordinary web requests. Because the checkout works out the price inside the shopper's own browser and the server simply trusts whatever number comes back, the buyer, not the business, sets the price.
The Vulnerability
The target sells group licenses for online continuing-education courses through a public group-purchase checkout. There is no login on this flow: anyone can start a bulk order.
The problem is where the price is decided. The checkout page computes the discounted order total in the browser, then posts the entire order object back to the server, including the client-supplied charge amount. The server takes that amount and reuses it verbatim to mint a live token for a hosted payment page. There is no server-side recompute from authoritative catalog prices, quantities, and the fixed bulk-discount policy. Whatever amount the client sends is the amount the shopper will be billed.
The client-side logic that produces the number looks roughly like this:
if (order.total_items > 4) {
order.charge_amount = order.total - (order.discount * order.total);
}
// then the whole order object, including charge_amount, is POSTed back
data: JSON.stringify({ group_request: order }) Because the mint endpoint accepts that number without validation, a buyer can substitute any value. There is only a small absolute charge floor near $63 that does not scale with order size, so the larger the true order, the closer the effective discount approaches 100 percent.
The Attack
The attack is two unauthenticated requests and requires no card data to demonstrate.
Baseline: an honest order
A legitimate 100-seat order at catalog price, with the intended group discount applied, mints a token whose hosted payment page charges the correct amount (roughly $4,670 for that order). This confirms the endpoint and the honest path.
curl -sS -H "Content-Type: application/json" -X POST \
"https://example-course-provider.tld/group-order/generate-token" \
--data '{"group_request":{"contact":{"name":"Test Buyer","email":"[email protected]"},"products":[{"name":"Course License","quantity":100,"price":54.95,"total":5495.00}],"order":{"total":5495.00,"total_items":100,"discount":0.15,"charge_amount":4670.75}}}' The resulting hosted payment page reports a charge amount matching the honest total.
Tampered: attacker-chosen amount
The same order, with the client-supplied charge amount replaced by 100.00, mints a token whose hosted page now charges $100. The server accepted the client number with no recompute, so the same product and quantity that should cost thousands now bills a hundred dollars.
curl -sS -H "Content-Type: application/json" -X POST \
"https://example-course-provider.tld/group-order/generate-token" \
--data '{... ,"order":{"total":5495.00,"total_items":100,"discount":0.15,"charge_amount":100.00}}'
# hosted page now reports totalAmount 100.00 Magnitude and blind spot at the processor
Scaling the order to 500 seats with a true subtotal near $64,500 and the same tampered amount still mints a hosted page that charges $100 - roughly a 99.8 percent undercharge. Critically, only the total amount and a static order description reach the payment processor. No line items, no per-seat price, and no quantity are sent. The processor therefore charges the tampered figure and has no data with which to detect the undercharge. The true order value exists only in the provider's own server-side order record.
A globally sequential invoice number returned by the mint endpoint also leaked total group-order volume as a side effect.
The Impact
An unauthenticated user can make the provider issue a live payment token for an arbitrary amount far below the true order value. That core defect is fully proven: the same 500-seat, ~$64,500 order reliably mints a payment page charging $100, and the processor receives no line items that would let it flag the mismatch. The order record persists with the real seat count and is queued for manual fulfillment ("access provided within 48 hours").
To be precise about scope, the end-to-end realized loss was deliberately not executed. Completing a payment would charge a real card and obtain course seats for a fraction of their price, which is real fraud and outside non-destructive testing limits. Two things therefore remain for the vendor to confirm in a controlled test:
- Whether the payment callback recomputes the catalog total and rejects a paid-amount mismatch. No reconciliation happens at mint time (proven), which is suggestive, but the callback path could behave differently.
- Whether manual fulfillment staff would notice the mismatch, since the order record carries the real seat count alongside the tampered amount.
The bottom line: the arbitrary-amount token mint is proven and unauthenticated; the completed-payment-to-fulfillment loop is the logical consequence but was not run.
Remediation
- Recompute the charge amount server-side from authoritative catalog prices, quantities, and the fixed bulk-discount policy before minting any token. Reject any request where the client-supplied amount does not match the server recompute.
- On the payment callback, re-derive the expected total and reject or flag any transaction whose paid amount does not match it. This is defense in depth, independent of the mint fix.
- Send line items (product, quantity, per-seat price) to the payment processor so an undercharge is detectable downstream.
- Validate product names and per-line totals against the catalog rather than trusting client-submitted values.
- Replace the globally sequential invoice number with a non-enumerable identifier so order volume is not disclosed.