Cafe Club: IDOR on Password Change
Overview
- Platform: BugForge
- Vulnerability: Insecure Direct Object Reference (IDOR)
- Key Technique: Password change endpoint uses
idfrom 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
- IDOR on Password Change Endpoint (CWE-639) — The
PUT /api/profile/passwordendpoint accepts a useridin 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-Byheader - 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 —roleandpointsare 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
idparameter. - 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
- CWE-639: Authorization Bypass Through User-Controlled Key
- OWASP IDOR Prevention Cheat Sheet
- OWASP Testing Guide: IDOR
- PortSwigger: Insecure Direct Object References (IDOR)
Tags: #IDOR #broken-access-control #account-takeover #password-change #nodejs #express #bugforge
Document Version: 1.0
Last Updated: 2026-03-28