BugForge — 2026.04.06

Cafe Club: Business Logic — Till Payment Bypass

BugForge Business Logic easy

Overview

  • Platform: BugForge
  • Vulnerability: Business Logic Flaw — Hidden Purchase Type Bypasses Payment
  • Key Technique: Fuzzing the checkout type parameter to discover an undocumented “till” value that zeroes out the order total
  • Result: Placed orders for $0 by exploiting the “till” checkout type intended for in-store POS use, captured flag from checkout response

Objective

Find and exploit vulnerabilities in a coffee shop e-commerce application to capture the flag.

Initial Access

# Target Application
URL: https://lab-1775415934488-8cg4i4.labs-app.bugforge.io

# Auth details
Registered user via POST /api/register
Auth mechanism: JWT HS256 (Authorization: Bearer header)
No token expiry observed

Key Findings

  1. Business Logic Flaw — Undocumented “till” Checkout Type Allows Free Purchases (CWE-840: Business Logic Errors) — The POST /api/checkout endpoint accepts a type parameter with two valid values: "online" (standard) and "till" (undocumented). The “till” type is intended for point-of-sale transactions where payment is collected at the physical register, so the API sets the total to $0. However, this type is accessible to any authenticated user via the API with no role or location validation, allowing free checkout of any cart contents.

Attack Chain Visualization

┌──────────────────┐     ┌──────────────────────┐     ┌──────────────────────┐
│  Register user   │     │ Map API surface      │     │  Test common         │
│  via /api/       │────>│ (auth, cart, orders, │────>│  vectors: mass       │
│  register        │     │ giftcards, checkout) │     │  assign, IDOR, SQLi  │
└──────────────────┘     └──────────────────────┘     └─────────┬────────────┘
                                                                │
                                                           All blocked
                                                                │
                                                                v
                                                    ┌────────────────────────┐
                                                    │  Fuzz checkout `type`  │
                                                    │  parameter — 24        │
                                                    │  candidates            │
                                                    └───────────┬────────────┘
                                                                │
                                                         "till" accepted
                                                                │
                                                                v
                                                    ┌────────────────────────┐
                                                    │  Add item to cart      │
                                                    │  POST /api/checkout    │
                                                    │  {"type":"till"}       │
                                                    └───────────┬────────────┘
                                                                │
                                                                v
                                                    ┌────────────────────────┐
                                                    │  200 OK — total: $0    │
                                                    │  Flag returned in      │
                                                    │  checkout response     │
                                                    └────────────────────────┘

Application Architecture

Component Path Description
Frontend React SPA Coffee shop storefront with products, cart, checkout
Backend Express/Node.js REST API with JWT authentication
Database SQLite (assumed, sequential IDs) Users, products, orders, gift cards
Auth JWT HS256 Token in Authorization: Bearer header, no expiry
Payments Checkout API Supports “online” (charges total) and “till” (zeroes total)

Exploitation Path

Step 1: Reconnaissance — Map the API Surface

Registered an account and captured traffic to enumerate all API endpoints. The application is a coffee shop e-commerce site with product browsing, cart management, gift cards, and checkout.

Key endpoints discovered:

  • POST /api/register / POST /api/login — Auth (returns JWT + user object)
  • GET /api/verify-token — Token validation
  • PUT /api/profile — Profile update (filters role and points fields)
  • PUT /api/profile/password — Password change (JWT-scoped, no old password required)
  • GET /api/products / GET /api/products/:id — Product catalog
  • GET/POST /api/cart / DELETE /api/cart/:id — Cart management
  • POST /api/checkout — Order placement with type parameter
  • GET /api/orders / GET /api/orders/:id — Order history (user-scoped)
  • POST /api/giftcards/purchase / POST /api/giftcards/redeem / GET /api/giftcards — Gift card system
  • GET/POST /api/products/:id/reviews — Product reviews
  • GET /api/favorites — Favorites (GET only, always empty)

Key observations:

  • Prices in euros, sequential IDs suggest SQLite
  • Profile update and registration both filter role and points — no mass assignment
  • Orders scoped to authenticated user via JWT (returns 404 for others, not 403)
  • Password change doesn’t require old password but is JWT-scoped — no IDOR vector

Step 2: Systematic Vector Testing (Dead Ends).. Mostly :|

Worked through common vulnerability classes, testing each individually:

Mass assignment — Both profile update and registration filter role and points fields. Server accepts the request but silently ignores protected fields. Tested on PUT /api/profile (role:”admin”, points:99999) and POST /api/register (role:”admin”, points:99999). All filtered.

IDOR on ordersGET /api/orders/1, /api/orders/2, /api/orders/3 all return 404 when authenticated as a different user. Properly scoped via JWT — clean implementation.

IDOR on password change — Attempted injecting id, user_id, and username fields into PUT /api/profile/password to target other users. All return 200 but only change our own password (verified by attempting login as the target). JWT-scoped, extra fields ignored.

Cart quantity manipulation — Negative (-1), zero (0), and float (0.5) quantities all rejected with “Valid product ID and quantity are required.”

Gift card abuse — Negative, zero, and sub-penny amounts rejected on purchase. Race condition on redeem (5 parallel requests) yielded exactly 1 success and 4 failures — atomic check, no double-credit.

Checkout points manipulation — Negative points_to_use rejected (“cannot be negative”). Over-balance values rejected (“Insufficient points”). Fully validated.

SQL injection — Tested ' OR 1=1-- on login and gift card redeem. No signal — parameterized queries likely.

JWT weak secret — Tested ~40 common secrets. No match — not trivially guessable.

Race condition — Tested (and found) race condition when checking out with gift cards. Can use gift cards multiple times without reduction in available balance.

IDOR on gift cards — Tested (and found) gift card can be redeemed once per user.

Hidden endpoints/api/admin, /api/flag, /api/users, /api/stats, /api/config all serve the SPA HTML (Express catch-all for non-API routes). No hidden API endpoints found.

Step 3: Checkout Type Fuzzing — Discovery

After exhausting standard vectors, the checkout type parameter stood out. Submitting type:"online" worked normally, but type:"in_store" returned “Invalid purchase type” — meaning the server validates against an enum. This raised the question: what other valid values exist?

Fuzzed with 24 candidates representing common POS/retail terminology:

POST /api/checkout HTTP/1.1
Host: lab-1775415934488-8cg4i4.labs-app.bugforge.io
Authorization: Bearer <jwt>
Content-Type: application/json

{"type":"till"}

Results:

  • till400 "Cart is empty" (accepted as valid type, but cart was emptied by prior test)
  • All 23 other values → 400 "Invalid purchase type"

The server accepts exactly two checkout types: online and till.

Step 4: Exploitation — Free Checkout via “till” Type

Added an item to the cart ($21.99) and performed a baseline comparison:

Online checkout (normal):

POST /api/checkout HTTP/1.1
Authorization: Bearer <jwt>
Content-Type: application/json

{"type":"online","points_to_use":0}

Response: 200 OKtotal: 21.99, points_earned: 21

Till checkout (exploit):

POST /api/checkout HTTP/1.1
Authorization: Bearer <jwt>
Content-Type: application/json

{"type":"till","points_to_use":0}

Response: 200 OKtotal: 0, points_earned: 0, flag in response body.

The “till” type is designed for in-store POS transactions where the physical register handles payment, so the API doesn’t charge. But the API exposes this type to any authenticated user without validating that the request originates from a POS terminal or staff account.


Flag / Objective Achieved

bug{4wpChqmrUME7uwrWmQtwiyOYlkHXXx1f}

Obtained from the checkout response when using type:"till".


Key Learnings

  • Enum fuzzing on constrained parameters is high-value — When a server validates input against an enum (rejecting "in_store" but accepting "online"), that’s a signal to fuzz for other valid values. The error message “Invalid purchase type” confirmed the enum pattern, and only 24 guesses were needed to find "till".
  • Business logic flaws survive when all technical controls are solid — This application had proper input validation, parameterized queries, JWT-scoped operations, mass assignment protections, and atomic race condition handling. The vulnerability was purely in the business logic — a checkout type that was never meant for external API consumers.
  • POS/payment type fields are high-risk in e-commerce APIs — Any field that controls how payment is processed (type, method, channel) is a prime target for manipulation. Different payment flows often have different validation logic, and internal-only flows sometimes skip payment collection entirely.
  • Dead ends inform the attack surface — Ruling out 12+ vectors (mass assignment, IDOR, SQLi, race conditions, JWT) narrowed focus to the checkout flow and specifically the type parameter. Systematic elimination is methodology, not wasted effort.

Failed Approaches

Approach Result Why It Failed
Mass assignment on profile (role/points) 200 but fields unchanged Server filters protected fields on update
Mass assignment on registration (role/points) 200 but defaults assigned Server filters protected fields on create
IDOR on orders 404 for other users’ orders Orders scoped to JWT identity
IDOR on password change (id/user_id/username injection) 200 but only self-changed Password endpoint JWT-scoped, extra fields ignored
Cart quantity manipulation (negative/zero/float) 400 validation error Strict server-side validation
Gift card amount manipulation 400 “Invalid gift card amount” Minimum threshold + strict validation
Gift card redeem race condition (5 parallel) 1 success, 4 failures Atomic redemption check
Checkout negative points 400 “cannot be negative” Server validates points >= 0
Checkout points > balance 400 “Insufficient points” Server checks actual balance
SQLi on login (' OR 1=1--) 400 “Invalid credentials” Parameterized queries
SQLi on gift card redeem 404 “Invalid gift card code” Parameterized queries
JWT weak secret brute force No match (40 candidates) Secret not trivially guessable
Hidden endpoint enumeration All serve SPA HTML No undocumented API routes

Tools Used

Tool Purpose
Caido HTTP interception, request replay, and parameter fuzzing
Browser DevTools Traffic capture and application inspection

Remediation

1. Business Logic Flaw — Unrestricted “till” Checkout Type (CVSS: 8.1 - High)

Issue: The /api/checkout endpoint accepts type:"till" from any authenticated user, setting the order total to $0. The “till” type is intended for in-store POS transactions but lacks role or origin validation. CWE Reference: CWE-840 - Business Logic Errors

// BEFORE (Vulnerable)
app.post('/api/checkout', authenticate, async (req, res) => {
  const { type, points_to_use } = req.body;
  if (!['online', 'till'].includes(type)) {
    return res.status(400).json({ error: 'Invalid purchase type' });
  }
  // "till" type sets total to 0 — no payment collected
  const total = type === 'till' ? 0 : calculateTotal(cart);
  // ... create order
});

// AFTER (Secure) — Option A: Restrict "till" to staff roles
app.post('/api/checkout', authenticate, async (req, res) => {
  const { type, points_to_use } = req.body;
  if (!['online', 'till'].includes(type)) {
    return res.status(400).json({ error: 'Invalid purchase type' });
  }
  if (type === 'till' && req.user.role !== 'staff') {
    return res.status(403).json({ error: 'Till checkout requires staff access' });
  }
  const total = type === 'till' ? 0 : calculateTotal(cart);
  // ... create order
});

// AFTER (Secure) — Option B: Remove "till" from customer-facing API
app.post('/api/staff/checkout', authenticate, requireRole('staff'), async (req, res) => {
  // POS checkout logic here — only accessible to staff
});

app.post('/api/checkout', authenticate, async (req, res) => {
  // Customer checkout — always calculates and charges the real total
  const total = calculateTotal(cart);
  // ... create order
});

Option B is preferred — separating customer and POS checkout into distinct endpoints eliminates the attack surface entirely. Internal payment flows should never share an endpoint with customer-facing ones.


OWASP Top 10 Coverage

  • A04:2021 - Insecure Design — The checkout flow exposes an internal business operation (POS till payment) to external API consumers without validating the request context. This is a design-level flaw, not an implementation bug.
  • A01:2021 - Broken Access Control — The “till” checkout type should be restricted to staff/POS roles but is accessible to any authenticated user.

References


Tags: #business-logic #payment-bypass #api-security #parameter-fuzzing #bugforge Document Version: 1.0 Last Updated: 2026-04-06

#payment-bypass #API #parameter-fuzzing