BugForge — 2026.04.11

OtterGram: IDOR on Profile Update

BugForge IDOR easy

Overview

  • Platform: BugForge
  • Vulnerability: Insecure Direct Object Reference (IDOR) — Arbitrary Profile Modification
  • Key Technique: Manipulating the id parameter in the PUT /api/profile request body to modify another user’s profile, bypassing JWT-based identity
  • Result: Overwrote admin’s profile data; flag recovered from admin’s bio field via public profile endpoint

Objective

Find the flag in the OtterGram social media application (BugForge lab).

Initial Access

# Target Application
URL: https://lab-1775948789079-octf0x.labs-app.bugforge.io

# Auth
POST /api/register — username, email, password, full_name → JWT + user object
POST /api/login — credentials → JWT + user object
JWT payload: {id, username, iat} (HS256, no expiration)

Key Findings

  1. IDOR on PUT /api/profile (CWE-639) — The server uses the id field from the JSON request body to determine which user’s profile to update, rather than deriving user identity from the authenticated JWT. Any authenticated user can modify any other user’s full_name and bio fields by changing the id value in the request body.

  2. Flag Stored in Admin Bio — The flag was stored in the admin user’s bio field, accessible via the public profile endpoint GET /api/profile/admin after the IDOR confirmed write access.


Attack Chain Visualization

┌──────────────────────────┐
│ POST /api/register       │
│ Create account (id: 4)   │
│ Receive JWT for haxor    │
└────────────┬─────────────┘
             │
             ▼
┌──────────────────────────┐
│Enumerate users via       │
│GET /api/profile/:username│
│Discover: admin (id: 2)   │
└────────────┬─────────────┘
             │
             ▼
┌──────────────────────────┐
│ PUT /api/profile         │
│ Body: {"id": 2,          │
│   "full_name": "test",   │
│   "bio": "test"}         │
│ JWT: haxor (id: 4)       │
└────────────┬─────────────┘
             │ Server uses body id (2)
             │ not JWT id (4)
             ▼
┌──────────────────────────┐
│ Admin profile modified   │
│ Response reveals original│
│ bio containing flag      │
└────────────┬─────────────┘
             │
             ▼
┌──────────────────────────┐
│ GET /api/profile/admin   │
│ Confirms modification +  │
│ flag visible in bio      │
│ bug{RVeqfYMa6O2A3B1M...} │
└──────────────────────────┘

Application Architecture

Component Path Description
Auth /api/register, /api/login, /api/verify-token JWT-based auth (HS256, no expiry), role from DB not token
Profile GET /api/profile/:username, PUT /api/profile Public profile view + profile update — IDOR on update
Posts POST /api/posts Multipart file upload for image posts
Comments POST /api/posts/:id/comments JSON comment creation
Likes POST /api/posts/:id/like, DELETE /api/posts/:id/like Like/unlike
Admin GET /api/admin Role-gated admin panel — returns flag area

Known Users

| ID | Username | Role | |—-|———-|——| | 1 | otter_lover | user | | 2 | admin | admin | | 3 | sea_otter_fan | user | | 4 | haxor (attacker) | user |


Exploitation Path

Step 1: Reconnaissance — Mapping the API

Registered an account and mapped the API surface. The application is an Express.js backend with a React SPA frontend, using JWT auth (HS256, no expiration) and likely SQLite. Key observations:

  • JWT payload contains {id, username, iat} — no role claim; role resolved from DB via /api/verify-token
  • PUT /api/profile accepts id, full_name, and bio in the request body
  • CORS is wide open (Access-Control-Allow-Origin: *)
  • Public profiles reveal user data including bio via GET /api/profile/:username

Step 2: Dead End — Mass Assignment (Role Escalation via Profile Update)

Hypothesis: The profile update endpoint might accept a role field, allowing privilege escalation from user to admin.

PUT /api/profile HTTP/1.1
Content-Type: application/json
Authorization: Bearer <jwt>

{
  "id": 4,
  "full_name": "test",
  "bio": "test",
  "role": "admin"
}

Result: 200 “Profile updated successfully”, but /api/verify-token still returned role: "user" and /api/admin returned 403.

Learning: Server whitelists mutable fields to full_name and bio only. The role field is silently dropped. Mass assignment on this endpoint is a dead end for privilege escalation.

Step 3: Dead End — Mass Assignment (Role Escalation via Registration)

Hypothesis: The registration code path may not filter the role field like the profile update does.

POST /api/register HTTP/1.1
Content-Type: application/json

{
  "username": "haxor2",
  "email": "[email protected]",
  "password": "password",
  "full_name": "",
  "role": "admin"
}

Result: 200, but response returned role: "user" — the role field was ignored.

Learning: Both register and profile update protect the role field. Mass assignment for role escalation is not viable on either endpoint.

Step 4: IDOR — Modifying Admin’s Profile via Body id Parameter

Hypothesis: The PUT /api/profile endpoint uses the id field from the request body for its WHERE clause instead of the authenticated user’s ID from the JWT. If true, we can modify any user’s profile by changing the id value.

PUT /api/profile HTTP/1.1
Content-Type: application/json
Authorization: Bearer <jwt>  (haxor, id: 4)

{
  "id": 2,
  "full_name": "IDOR test",
  "bio": "test"
}

Result: 200 “Profile updated successfully”. The server accepted the request and applied the update to user ID 2 (admin) instead of user ID 4 (our JWT identity).

Step 5: Confirming the IDOR and Recovering the Flag

Fetched the admin’s public profile to verify the modification:

GET /api/profile/admin HTTP/1.1
Authorization: Bearer <jwt>

Result: Admin’s full_name was changed to “IDOR test” and bio field now contained our test value alongside the original bio content, which included the flag: bug{RVeqfYMa6O2A3B1MRYq7JGsJXI4K8YR7}

The flag was stored in the admin’s bio field — a piece of data that was only accessible by either having admin credentials or modifying the profile to reveal it through the public profile endpoint.


Flag / Objective Achieved

bug{RVeqfYMa6O2A3B1MRYq7JGsJXI4K8YR7}

Key Learnings

  • Body parameter id as authorization bypass. When a server accepts a resource identifier in the request body for an update operation and uses that instead of the authenticated session identity, it creates a classic IDOR. The fix is straightforward: always derive the target resource from the authenticated context (JWT claims, session), never from user-controlled input.

  • Role-in-DB vs role-in-token matters for attack surface. The role was stored only in the database and resolved via /api/verify-token, not embedded in the JWT. This meant role escalation required modifying the DB value (which was protected by field whitelisting), not forging a token. However, it also meant the IDOR on profile update couldn’t directly escalate privileges — only modify profile fields.

  • Dead ends narrow the attack surface productively. Testing mass assignment on both profile update and registration confirmed that the server protects the role field on both code paths. This ruled out privilege escalation and redirected focus to the IDOR vector, which turned out to be the intended path.


Failed Approaches

Approach Result Why It Failed
Mass assignment role: "admin" via PUT /api/profile 200 OK but role unchanged Server whitelists fields to full_name, bio only
Mass assignment role: "admin" via POST /api/register Account created with role “user” Registration code ignores role field, always sets “user”

Tools Used

Tool Purpose
Caido HTTP proxy for intercepting and replaying API requests
Browser DevTools Analyzing React bundle and network traffic
curl Direct API testing for IDOR payload

Remediation

1. Insecure Direct Object Reference on Profile Update (CVSS: 7.1 - High)

CVSS Vector: AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N

Issue: The PUT /api/profile endpoint uses the id field from the request body to determine which user’s profile to update. Any authenticated user can modify any other user’s profile data by supplying a different id value.

CWE Reference: CWE-639 — Authorization Bypass Through User-Controlled Key

Fix:

// BEFORE (Vulnerable)
app.put('/api/profile', authMiddleware, (req, res) => {
  const { id, full_name, bio } = req.body;
  // Uses attacker-controlled id for the update
  db.run('UPDATE users SET full_name = ?, bio = ? WHERE id = ?',
    [full_name, bio, id]);
});

// AFTER (Secure)
app.put('/api/profile', authMiddleware, (req, res) => {
  const { full_name, bio } = req.body;
  // Always use the authenticated user's ID from the JWT
  db.run('UPDATE users SET full_name = ?, bio = ? WHERE id = ?',
    [full_name, bio, req.user.id]);
});

2. Sensitive Data in User Profile Field (CVSS: 4.3 - Medium)

CVSS Vector: AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N

Issue: The flag (representing sensitive data) was stored in the admin user’s bio field, which is exposed via the public profile endpoint. Sensitive data should not be stored in user-editable profile fields that are publicly accessible.

CWE Reference: CWE-200 — Exposure of Sensitive Information to an Unauthorized Actor

Fix: Store sensitive administrative data in protected database fields that are not exposed through public-facing API endpoints. Profile fields like bio should only contain user-generated content, not application secrets.


OWASP Top 10 Coverage

  • A01:2021 — Broken Access Control — The IDOR vulnerability is a direct example of broken access control: the server fails to validate that the authenticated user is authorized to modify the target resource, relying on a user-controlled parameter instead of the authenticated session.
  • A04:2021 — Insecure Design — Accepting a resource identifier in the request body for an update operation without server-side ownership validation is a design-level flaw. The API should be designed so that the target of a profile update is always derived from the authenticated context.

References


Tags: #idor #broken-access-control #express #jwt #profile-manipulation #bugforge #bola Document Version: 1.0 Last Updated: 2026-04-11

#idor #broken-access-control #express #jwt #profile-manipulation #bola