OtterGram: IDOR on Profile Update
Overview
- Platform: BugForge
- Vulnerability: Insecure Direct Object Reference (IDOR) — Arbitrary Profile Modification
- Key Technique: Manipulating the
idparameter 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
-
IDOR on PUT /api/profile (CWE-639) — The server uses the
idfield 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’sfull_nameandbiofields by changing theidvalue in the request body. -
Flag Stored in Admin Bio — The flag was stored in the admin user’s
biofield, accessible via the public profile endpointGET /api/profile/adminafter 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/profileacceptsid,full_name, andbioin 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
idas 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
rolefield 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
- CWE-639: Authorization Bypass Through User-Controlled Key
- CWE-200: Exposure of Sensitive Information
- OWASP Testing Guide — IDOR
- OWASP API Security Top 10 — BOLA
- PortSwigger — Insecure Direct Object References
Tags: #idor #broken-access-control #express #jwt #profile-manipulation #bugforge #bola
Document Version: 1.0
Last Updated: 2026-04-11