BugForge — 2026.03.21

Ottergram: WebSocket IDOR via Socket.io

BugForge Broken Access Control medium

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-message event 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

  1. IDOR via Socket.io preview-message event (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/register and /api/profile — role fields filtered server-side
  • IDOR on REST messages/api/messages/inbox scoped to JWT user, PATCH /api/messages/:id/read checks 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 dangerouslySetInnerHTML in 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-message handler had zero ownership validation.
  • JS bundles are a goldmine for attack surface discovery. The preview-message event 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


Tags: #idor #websocket #socket-io #broken-access-control #bugforge #message-disclosure Document Version: 1.0 Last Updated: 2026-03-21

#IDOR #WebSocket #Socket.io #message-enumeration