BugForge — 2026.03.28

Cafe Club: IDOR on Password Change

BugForge Broken Access Control easy

Overview

  • Platform: BugForge
  • Vulnerability: Insecure Direct Object Reference (IDOR)
  • Key Technique: Password change endpoint uses id from request body instead of JWT — any authenticated user can change any other user’s password
  • Result: Full account takeover of user ID 1 (admin); flag captured

Objective

Find and exploit a vulnerability in the Cafe Club coffee shop loyalty program web application.

Initial Access

# Target Application
URL: https://lab-1774740774073-puatgz.labs-app.bugforge.io

# Auth details
# Registered via POST /api/register
Username: haxor
User ID: 5
Auth: JWT HS256 Bearer token (no expiry)
JWT payload: {"id":5,"username":"haxor","iat":...}

Key Findings

  1. IDOR on Password Change Endpoint (CWE-639) — The PUT /api/profile/password endpoint accepts a user id in the JSON request body and uses it to determine whose password to update, rather than deriving the target user from the authenticated JWT. Any authenticated user can overwrite any other user’s password, achieving full account takeover.

Attack Chain Visualization

┌──────────────┐    ┌──────────────────┐    ┌─────────────────────┐
│  Register    │───▶│  Map API Surface │───▶│  Test Mass          │
│  Account     │    │  (17 endpoints)  │    │  Assignment on      │
│  (ID 5)      │    │                  │    │  PUT /api/profile   │
└──────────────┘    └──────────────────┘    └─────────┬───────────┘
                                                      │
                                                      │ role/points
                                                      │ filtered ✗
                                                      ▼
                                           ┌─────────────────────┐
                                           │  Examine password   │
                                           │  change endpoint    │
                                           │  PUT /api/profile/  │
                                           │  password           │
                                           └─────────┬───────────┘
                                                      │
                                                      │ body accepts
                                                      │ {"password","id"}
                                                      ▼
                                           ┌─────────────────────┐
                                           │  Send id:1 from     │
                                           │  user 5's session   │
                                           │  → User 1 password  │
                                           │    changed ✓        │
                                           │  → Flag returned    │
                                           └─────────────────────┘

Application Architecture

Component Path Description
Auth POST /api/register, POST /api/login, GET /api/verify-token JWT HS256 registration, login, and validation
Products GET /api/products, GET /api/products/:id Catalog (16 products across Coffee Beans, Equipment, Accessories)
Cart/Checkout GET/POST /api/cart, POST /api/checkout Shopping flow; card payments earn ~1 pt/$1
Profile GET/PUT /api/profile, PUT /api/profile/password User profile and password change (vulnerable)
Gift Cards GET /api/giftcards, POST /api/giftcards/purchase, POST /api/giftcards/redeem Gift card purchase and redemption
Orders GET /api/orders, GET /api/orders/:id Order history
Favorites GET/POST/DELETE /api/favorites Product favorites

Exploitation Path

Step 1: Reconnaissance — API Surface Mapping

Registered an account and explored the application, capturing HTTP traffic. Identified 17 API endpoints across authentication, products, cart/checkout, profile, gift cards, orders, and favorites.

Key observations:

  • Backend is Express (Node.js) per X-Powered-By header
  • Auth uses JWT HS256 with payload {"id":5,"username":"haxor","iat":...} — no expiry
  • Points system: ~1 point per $1 on card payments, gift card payments earn 0 points
  • Pre-seeded users with IDs 1-4; our registered user is ID 5

Step 2: Mass Assignment Testing (Dead End)

Tested the PUT /api/profile endpoint for mass assignment — a vulnerability that worked in a previous iteration of this lab (03-15). This time the server properly filters both role and points fields:

PUT /api/profile HTTP/2
Host: lab-1774740774073-puatgz.labs-app.bugforge.io
Authorization: Bearer <jwt>
Content-Type: application/json

{"full_name":"haxor","email":"h@h.com","address":"x","phone":"x","role":"admin","points":99999}

Result: role stayed “user”, points stayed at 21. Server-side filtering in place.

Step 3: Password Change Endpoint Analysis

Examined the PUT /api/profile/password endpoint. The request body structure revealed a critical flaw — it accepts an id field:

PUT /api/profile/password HTTP/2
Host: lab-1774740774073-puatgz.labs-app.bugforge.io
Authorization: Bearer <jwt>
Content-Type: application/json

{"password":"hacked123","id":1}

The endpoint uses the id from the request body to determine whose password to update, rather than extracting the user ID from the authenticated JWT token. This means any authenticated user can change any other user’s password.

Step 4: Account Takeover — Flag Captured

Sent the request with id:1 (first pre-seeded user) from user 5’s authenticated session:

{"message":"Profile updated successfully. bug{3edK1T49sTavBeGez8pCXpL3ztoY9s6A}"}

User 1’s password was changed to “hacked123”. The flag was returned inline in the success response.


Flag / Objective Achieved

bug{3edK1T49sTavBeGez8pCXpL3ztoY9s6A}

Returned in the response body of PUT /api/profile/password after changing user 1’s password from user 5’s session.


Key Learnings

  • Always check what identifies the target user in write operations. If an endpoint accepts a user ID in the request body rather than deriving it from the session/token, it’s almost certainly an IDOR. The JWT existed and was validated for authentication — but authorization used a client-controlled value.
  • Patching one vuln can expose another. The mass assignment on PUT /api/profile (which worked on 03-15) was fixed here — role and points are now filtered. But the password change endpoint introduced a new IDOR by trusting body parameters for user identification.
  • Separate endpoints for profile updates and password changes can have inconsistent authorization logic. The main profile endpoint (PUT /api/profile) correctly derives the user from the JWT token. The password endpoint (PUT /api/profile/password) does not — a classic case of inconsistent authorization across related endpoints.
  • Pre-seeded user IDs are predictable. Sequential integer IDs (1-4 for seed data, 5+ for registered users) make IDOR exploitation trivial once the vulnerability is found.

Failed Approaches

Approach Result Why It Failed
Mass assignment on PUT /api/profile (role) role stays “user” Server-side field filtering — only full_name, address, phone, email writable
Mass assignment on PUT /api/profile (points) points stays at 21 Server-side field filtering — points not in allowed update fields

Tools Used

Tool Purpose
Caido HTTP proxy for request interception, API mapping, and replay
Browser DevTools Initial application exploration

Remediation

1. IDOR on Password Change (CVSS: 9.8 - Critical)

Issue: PUT /api/profile/password uses the id field from the JSON request body to determine which user’s password to change. Any authenticated user can change any other user’s password by supplying a different id.

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

Fix:

// BEFORE (Vulnerable)
app.put('/api/profile/password', auth, async (req, res) => {
  const { password, id } = req.body;
  const hashed = await bcrypt.hash(password, 10);
  await User.update({ password: hashed }, { where: { id } });
  res.json({ message: 'Profile updated successfully' });
});

// AFTER (Secure)
app.put('/api/profile/password', auth, async (req, res) => {
  const { password } = req.body;
  const hashed = await bcrypt.hash(password, 10);
  // Always use the authenticated user's ID from the JWT
  await User.update({ password: hashed }, { where: { id: req.user.id } });
  res.json({ message: 'Profile updated successfully' });
});

Additional defense-in-depth measures:

  • Require the current password before allowing a password change
  • Rate-limit password change requests per user session
  • Log and alert on password changes, especially for privileged accounts
  • Audit all endpoints that accept user IDs in request bodies — derive identity from the session/token instead

OWASP Top 10 Coverage

  • A01:2021 — Broken Access Control: The password change endpoint allows horizontal privilege escalation — any user can modify any other user’s password by controlling the id parameter.
  • A04:2021 — Insecure Design: The endpoint was designed to accept a user-controlled key for a privileged operation (password change) rather than deriving the target from the authenticated session.

References


Tags: #IDOR #broken-access-control #account-takeover #password-change #nodejs #express #bugforge Document Version: 1.0 Last Updated: 2026-03-28

#IDOR #password-change #account-takeover