BugForge — 2026.09.12

Cheesy Does It: Mass Assignment via Hidden PATCH Verb

BugForge Mass Assignment via Hidden Update Verb easy

Executive Summary

“Cheesy Does It” is a pizza ordering web app on the BugForge platform: a Create React App single page application (MUI components) over a Node.js and Express backend, with JWT (HS256) authentication and a SQL-style datastore. Customers register, build an order, pay with a card, and track the order through delivery. The advertised objective is a flag that sits behind privileged behavior, and the lab hint for this rotation was “mass assignment”.

Testing confirmed one finding:

ID Title Severity CVSS CWE Endpoint
F1 Mass assignment on the order update verb re-prices an order Medium 6.5 CWE-915, CWE-472 PATCH /api/orders/:id

The order create path (POST /api/orders) recomputes the order total on the server and ignores any total_price sent by the client, which closes price tampering on order creation. The order update path does not share that guard: PATCH /api/orders/:id binds total_price straight from the request body and persists it. The PATCH verb is never issued by the single page application (which only calls GET /api/orders/:id and POST /api/orders), so it does not appear in an endpoint map built from the frontend bundle or from proxy history. Sending PATCH /api/orders/6 with {"total_price": 10} against one of our own orders re-priced it and returned the flag bug{7jc7OOpg2gWZg5XoRzHzcizhX7UcPjNL} in the response body.


Objective

Recover the lab flag by finding and exercising a mass assignment defect on the BugForge “Cheesy Does It” application.


Scope / Initial Access

# Target Application
URL: https://lab-1789232601685-1i6hu1.labs-app.bugforge.io

# Auth
POST /api/register  -> {username, email, password, full_name, phone, address} -> JWT HS256
POST /api/login     -> {username, password} -> JWT HS256
                       payload: {"id":N, "username":"...", "iat":...}  (no role claim)
Authorization: Bearer <jwt>   on protected endpoints

# Test account
haxor (id=8, role=user), created via standard self-registration

Self-registration is open. Registration and login both return {token, user}. The JWT does not carry a role claim; GET /api/verify-token returns the role on demand, and the server re-reads the role from the database on every authenticated request. Because the role lives server side rather than in the token, any privilege change would need a server side write to the user record, not a token edit.


Reconnaissance: Mapping the Order Resource by Verb

The application is a small React SPA with an enumerable API surface. Reading the bundle (/static/js/main.0de85fb7.js) and walking the checkout flow once produced the endpoint inventory. Three observations shaped the test plan:

  1. The frontend endpoint map lists only the verbs the client calls. For the order resource the client emits POST /api/orders (create) and GET /api/orders/:id (read). No update verb on the detail route is emitted by the SPA, so a map derived from the bundle shows the order detail route as read only even though the server may register more verbs on it.
  2. The create path recomputes the order total on the server. A total_price (and status, points_earned, user_id, discount_total) injected into POST /api/orders was ignored, and the response carried the server calculated total. This normally closes price tampering at order creation, and it raised the question of whether the update path applies the same recomputation.
  3. Role is read server side and the user write sinks whitelist their fields. Register and profile updates accept a fixed field set and drop everything else, so setting a privileged role through those endpoints does not work (see Failed Approaches). With the role path closed and the create path recomputing, the untested surface was the order update verb the client never calls.

Application Architecture

Component Detail
Frontend React SPA (Create React App, MUI), bundle at /static/js/main.0de85fb7.js
Backend Node.js and Express (a POST to an unknown path returns the Express Cannot POST 404; unknown GET paths fall to the SPA catchall)
Auth JWT HS256, Authorization: Bearer ...; payload {id, username, iat}, no role claim; role read via GET /api/verify-token
Datastore SQL-style (response timestamps such as 2026-09-12 17:29:55 suggest SQLite or MySQL)
Order lifecycle Status auto-advances on the server over time (received through out for delivery); the update verb is accepted only while the order is pre-dispatch

API Surface

Endpoint Method Auth Notes
/api/register POST none username, email, password, full_name, phone, address (field whitelist)
/api/login POST none username, password
/api/verify-token GET bearer returns the user record including role
/api/profile PUT bearer full_name, email, phone, address (field whitelist)
/api/orders POST bearer create; total and status computed on the server
/api/orders/:id GET bearer read a single order
/api/orders/:id PATCH bearer update; binds total_price from the body (this finding)
/api/payment/validate, /api/payment/process POST bearer card validation and charge
/api/admin/* GET/PUT/POST admin 403 for a non-admin token, 401 without a token

Known Users

Username ID Role
admin 1 (seeded, inferred) admin
haxor 8 user (our test account)

Attack Chain Visualization

┌──────────────────┐   ┌───────────────────────────┐   ┌──────────────────────────────┐   ┌───────────────────────────┐
│  Register        │   │  Create an order          │   │  PATCH the order detail route │   │  200 OK                   │
│  POST            │──▶│  POST /api/orders         │──▶│  PATCH /api/orders/6          │──▶│  order total set to 10    │
│  /api/register   │   │  total computed server    │   │  {"total_price": 10}          │   │  flag in the response body│
│  -> user JWT     │   │  side, status "received"  │   │  (verb the SPA never calls)   │   │                           │
└──────────────────┘   └───────────────────────────┘   └──────────────────────────────┘   └─────────────┬─────────────┘
                                                                                                         │
                                                                                                         ▼
                                                        ┌────────────────────────────────────────────────────────────┐
                                                        │  bug{7jc7OOpg2gWZg5XoRzHzcizhX7UcPjNL}                       │
                                                        └────────────────────────────────────────────────────────────┘

Findings

F1: Mass assignment on the order update verb re-prices an order

Severity: Medium CVSS v3.1: 6.5 (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N) CWE: CWE-915 (Improperly Controlled Modification of Dynamically-Determined Object Attributes), CWE-472 (External Control of Assumed-Immutable Web Parameter) Endpoint: PATCH /api/orders/:id Authentication required: Yes, any registered user JWT

Description

The order create and update paths handle the total_price field differently:

  1. Create (POST /api/orders) recomputes the total server side. A total_price supplied in the create body is ignored, and the response returns the server calculated value.
  2. Update (PATCH /api/orders/:id) binds total_price from the request body. The value in the request is written to the order record and persisted, with no recomputation from the menu prices.

Because the two verbs are separate handlers, the field whitelist on create says nothing about update. The update handler takes the client value directly, so an authenticated customer can re-price one of their own orders by sending a PATCH to its detail route.

The PATCH verb on /api/orders/:id is not exercised by the single page application, which issues only GET /api/orders/:id and POST /api/orders. An endpoint map derived from the frontend bundle or from proxy history therefore shows the order detail route as read only and does not surface the update verb at all.

Precondition (timing gate): the update is accepted only while the order is still pre-dispatch (status received or early). Once the status advances to out for delivery the update is refused. Orders auto-advance on the server over time, so the PATCH has to land shortly after the order is created. The order used here (id 6) was in status received, and the update timestamp (17:30:46) is later than the create timestamp (17:29:55), inside the window.

Impact

Would allow an authenticated customer to re-price their own order below the amount owed.

Reproduction

Step 1: Register and capture a user JWT

POST /api/register HTTP/1.1
Host: lab-1789232601685-1i6hu1.labs-app.bugforge.io
Content-Type: application/json

{"username":"haxor","email":"h@x","password":"p","full_name":"H","phone":"0","address":"x"}

Response: 200 OK with a JWT. The token decodes to {"id":8,"username":"haxor","iat":...}. Save it as USER_JWT.

Step 2: Create an order through the normal checkout

Complete a standard checkout (the lab’s valid test card is 4444 4444 4444 4444, exp 12/25, cvv 123). This produces an order that belongs to our account (user_id 8) in status received, with a server calculated total. The order created here was id 6, order_number CDI-1789234195894-MY2WQX0D4.

Step 3: While the order is pre-dispatch, PATCH the detail route with a chosen total

PATCH /api/orders/6 HTTP/1.1
Host: lab-1789232601685-1i6hu1.labs-app.bugforge.io
Authorization: Bearer <USER_JWT>
Content-Type: application/json
Content-Length: 20

{"total_price": 10}

Response:

{
  "message": "Order updated successfully",
  "order": {
    "id": 6,
    "user_id": 8,
    "order_number": "CDI-1789234195894-MY2WQX0D4",
    "total_price": 10,
    "status": "received",
    "delivery_address": "17 Bugforge Road",
    "phone": "0123456789",
    "payment_method": "card",
    "notes": "",
    "coupon_code": null,
    "points_used": 0,
    "discount_total": 0,
    "created_at": "2026-09-12 17:29:55",
    "updated_at": "2026-09-12 17:30:46"
  },
  "flag": "bug{7jc7OOpg2gWZg5XoRzHzcizhX7UcPjNL}"
}

The injected total_price of 10 is echoed and persisted (updated_at is later than created_at), and the lab appends the flag key to the response on the successful mutation.

Remediation

Fix 1: Recompute the total on update instead of binding it from the body

// BEFORE (Vulnerable: total_price is taken from the request body)
const { total_price, delivery_address, phone, notes } = req.body;
db.run(
  'UPDATE orders SET total_price = ?, delivery_address = ?, phone = ?, notes = ? WHERE id = ? AND user_id = ?',
  [total_price, delivery_address, phone, notes, orderId, req.user.id]
);

// AFTER (Secure: never accept a client total; recompute from stored items)
const { delivery_address, phone, notes } = req.body;   // total_price not read from the client
const computedTotal = await recomputeOrderTotal(orderId);   // same server side pricing as create
db.run(
  'UPDATE orders SET total_price = ?, delivery_address = ?, phone = ?, notes = ? WHERE id = ? AND user_id = ?',
  [computedTotal, delivery_address, phone, notes, orderId, req.user.id]
);

Additional recommendations:

  • Apply the same field whitelist on update that the create path already uses. The update handler should accept only the fields a customer is allowed to change (for example delivery address and notes) and derive every price, status, and points field on the server.
  • Treat create and update as the surface they are: any field the create path computes or drops must be computed or dropped on the update path as well. A guard on one verb does not carry to the others.
  • Add an integrity check on the order pipeline that flags an order whose stored total deviates from the sum of its menu priced items, so a re-priced order is caught after the fact.

OWASP Top 10 Coverage

  • A01:2021 Broken Access Control: A customer can modify a field on their own order (total_price) that they must not control. The update handler enforces ownership (the WHERE clause binds the order to the caller) but not field level authorization over what may be changed.
  • A04:2021 Insecure Design: The update path trusts a client supplied total with no recomputation, while the create path recomputes. Pricing integrity depends on which verb reaches the record, which is a design gap rather than a single coding slip.

Tools Used

Tool Purpose
Caido Request capture, replay, and tampering (including the winning PATCH)
ffuf Endpoint and path discovery (filtered against the 790-byte SPA catchall)
Browser DevTools Reading the React bundle and inspecting the stored JWT and cart state
JS bundle review Extracting the client’s endpoint and verb map from main.0de85fb7.js

References


Failed Approaches

Approach Result Why It Failed
Mass assignment of role / isAdmin / is_admin / roles[] / permissions on POST /api/register Role stayed user Registration destructures a fixed field whitelist; extra fields are dropped
Same privileged fields on PUT /api/profile Role stayed user Profile update uses the same field whitelist
Injected total_price / status / points_earned / user_id on POST /api/orders (create) All ignored The create handler recomputes the total and status server side
Injected user_id / status on tickets, username / user_id on reviews Stored as our own identity Both handlers whitelist their fields
Prototype pollution (__proto__, constructor.prototype with isAdmin / role) via the write endpoints No privilege change No admin flip from a polluted prototype
form-urlencoded body on POST /api/register 400 The body parser is JSON only
Sibling and versioned mounts for profile and register (/v1, /v2, /api/v1, unversioned) 404 (SPA catchall) No legacy or versioned mount exists
Verb-asymmetric access control on /api/admin/* (POST / PUT / DELETE with a non-admin token) 403 uniformly The admin middleware is applied uniformly across verbs
Cross-user profile write via an injected {id} Updated our own record The WHERE clause binds to the JWT id; the body id is not bound
Register with the existing admin username 400, original account unchanged No upsert or overwrite on a username collision

Tags: #mass-assignment #verb-tampering #hidden-handler #business-logic #bugforge #webapp Document Version: 1.0 Last Updated: 2026-09-12

#mass-assignment #verb-tampering #hidden-handler #business-logic #bugforge